Odoo provides almost all fields that are required for specific functionality. But in certain business scenarios, we have to add fields and customize accordingly. From an end-user, it is difficult to create the fields from code. So here comes the need for dynamic field creation in odoo.
In this blog, we are discussing dynamic field creation in odoo.
First of all we need a wizard to create the field. So the wizard.py file is as follows,
class EmployeeDynamicFields(models.TransientModel):
_name = 'employee.dynamic.fields'
_description = 'Dynamic Fields'
_inherit = 'ir.model.fields'
position_field = fields.Many2one('ir.model.fields', string='Field Name', required=True)
position = fields.Selection([('before', 'Before'),
('after', 'After')], string='Position', required=True)
model_id = fields.Many2one('ir.model', string='Model', required=True, index=True, ondelete='cascade',
help="The model this field belongs to")
ref_model_id = fields.Many2one('ir.model', string='Model', index=True)
# In odoo 13 the field 'selection' is deprecated, so adding a new field to get the selection values.
selection_field = fields.Char(string="Selection Options")
rel_field = fields.Many2one('ir.model.fields', string='Related Field')
field_type = fields.Selection(selection='get_possible_field_types', string='Field Type', required=True)
ttype = fields.Selection(string="Field Type", related='field_type')
extra_features = fields.Boolean(string="Show Extra Properties")
The wizard should contain the fields to choose the model, reference model, type of the field(binary,char,datetime etc), related field which is to create the field with respect to an existing field. Also, the position is to identify whether to add the field after or before the existing field.
To display the types of the field we add selection=get_possible_field_types to the field, field_type. And the method get_possible_field_types is as follows
@api.model
def get_possible_field_types(self):
"""Return all available field types other than 'one2many' and 'reference' fields."""
field_list = sorted((key, key) for key in fields.MetaField.by_type)
field_list.remove(('one2many', 'one2many'))
field_list.remove(('reference', 'reference'))
return field_list
Each field has a different widget in the view, such as many2many_tags,radio etc . To return the widget as per the field_type, we have a method on changing the field_type as given below:
@api.depends('field_type')
@api.onchange('field_type')
def onchange_field_type(self):
if self.field_type:
if self.field_type == 'binary':
return {'domain': {'widget': [('name', '=', 'image')]}}
elif self.field_type == 'many2many':
return {'domain': {'widget': [('name', 'in', ['many2many_tags', 'binary'])]}}
elif self.field_type == 'selection':
return {'domain': {'widget': [('name', 'in', ['radio', 'priority'])]}}
elif self.field_type == 'float':
return {'domain': {'widget': [('name', '=', 'monetary')]}}
elif self.field_type == 'many2one':
return {'domain': {'widget': [('name', '=', 'selection')]}}
else:
return {'domain': {'widget': [('id', '=', False)]}}
return {'domain': {'widget': [('id', '=', False)]}}
This method will return with a widget as per the field_type.
The view of the widget is as follows ie, wizard.xml:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model='ir.ui.view' id='wizard_employee_dynamic_fields_form'>
<field name="name">employee.dynamic.fields.form</field>
<field name="model">employee.dynamic.fields</field>
<field name="arch" type="xml">
<form string="Dynamic Fields">
<sheet>
<group>
<group string="Field Info">
<field name="name"/>
<field name="field_description"/>
<field name="state" readonly="1" groups="base.group_no_one"/>
<field name="model_id" options='{"no_open": True, "no_create": True}'/>
<field name="field_type"/>
<field name="selection_field" placeholder="[('blue', 'Blue'),('yellow', 'Yellow')]"
attrs="{'required': [('field_type','in',['selection','reference'])],
'readonly': [('field_type','not in',['selection','reference'])],
'invisible': [('field_type','not in',['selection','reference'])]}"/>
<field name="ref_model_id" options='{"no_open": True, "no_create": True}' attrs="{'required': [('field_type','in',['many2one','many2many'])],
'readonly': [('field_type','not in',['many2one','many2many'])],
'invisible': [('field_type','not in',['many2one','many2many'])]}"/>
<field name="widget" widget="selection"
attrs="{'invisible': [('field_type','not in',['binary', 'many2many', 'selection', 'float', 'many2one'])]}"/>
<field name="required"/>
</group>
<group string="Position">
<field name="position_field" options='{"no_open": True, "no_create": True}'/>
<field name="position"/>
</group>
</group>
<group string="Extra Properties">
<group>
<field name="extra_features"/>
</group>
<group attrs="{'invisible': [('extra_features', '=', False)]}">
<field name="help"/>
</group>
<group attrs="{'invisible': [('extra_features', '=', False)]}">
<field name="readonly"/>
<field name="store"/>
<field name="index"/>
<field name="copied"/>
</group>
</group>
</sheet>
<footer>
<button name="create_fields" string="Create Fields" type="object" class="oe_highlight"/>
or
<button string="Cancel" class="oe_link" special="cancel"/>
</footer>
</form>
</field>
</record>
<record model='ir.actions.act_window' id='action_employee_dynamic_fields'>
<field name="name">Create Custom Fields</field>
<field name="res_model">employee.dynamic.fields</field>
<field name="view_mode">form</field>
<field name="view_id" ref="wizard_employee_dynamic_fields_form"/>
<field name="target">new</field>
</record>
<!-- Menu Item in Employee to create fields -->
<menuitem
id="menu_create_employee_fields"
name="Create Fields"
parent="hr.menu_hr_employee_payroll"
action="employee_dynamic_fields.action_employee_dynamic_fields"
sequence="10"/>
</odoo>
In this example, I am creating dynamic fields in the model hr.employee. We can add groups for the wizard menu if required.
And the output wizard is given below:
Next comes the method to create the field. In this example, wizard contains a create field button, on clicking that button the method create_fields executes
def create_fields(self):
self.env['ir.model.fields'].sudo().create({'name': self.name,
'field_description': self.field_description,
'model_id': self.model_id.id,
'ttype': self.field_type,
'relation': self.ref_model_id.model,
'required': self.required,
'index': self.index,
'store': self.store,
'help': self.help,
'readonly': self.readonly,
'selection': self.selection_field,
'copied': self.copied,
'is_employee_dynamic': True
})
inherit_id = self.env.ref('hr.view_employee_form')
arch_base = _('<?xml version="1.0"?>'
'<data>'
'<field name="%s" position="%s">'
'<field name="%s"/>'
'</field>'
'</data>') % (self.position_field.name, self.position, self.name)
if self.widget:
arch_base = _('<?xml version="1.0"?>'
'<data>'
'<field name="%s" position="%s">'
'<field name="%s" widget="%s"/>'
'</field>'
'</data>') % (self.position_field.name, self.position, self.name, self.widget.name)
self.env['ir.ui.view'].sudo().create({'name': 'employee.dynamic.fields.%s' % self.name,
'type': 'form',
'model': 'hr.employee',
'mode': 'extension',
'inherit_id': inherit_id.id,
'arch_base': arch_base,
'active': True})
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
In this method, we are creating the record i.e., field in the model ir.model.fields with the data that has been retrieved from the wizard. In this example I have added in the employee form, only the inherit_id is given as the reference of the employee form. If we are giving for multiple models, it has to change accordingly. So that view is created based on the inherit_id. To set the widget in the xml, we have arch_base. Also, we need to create these records in the ir.ui.view only then, corresponding fields can be viewed. Once we are done with the record in both the models, we can just reload ir.actions.client.
Let us see how it works in the user-interface,
Here I am creating a field in the model hr.employee of the field_type datetime which is a required field and the position is after the existing field Marital status.
And the result is as follows: