How to Create Custom Fields Dynamically in Odoo 18
By Braincuber Team
Published on January 15, 2026
Business requirements change faster than development cycles. One day your Project Managers track "Estimated Days", the next they need "Site Coordinates" or "Client Device Type". Hard-coding these fields every time creates technical debt and bottlenecks.
In this advanced tutorial, we will build a Dynamic Field Generator for Odoo 18. Instead of static declarations, we'll create a wizard that allows authorized users to generate custom fields and inject them into views programmatically using Python functions. We'll apply this to the Project Task model, giving PMs the power to adapt their workspace instantly.
What You Will Build
A wizard that:
- Takes a field name, type, and position (Before/After existing fields).
- Creates a record in
ir.model.fields. - Dynamically extends the Form View using
ir.ui.viewinheritance. - Reloads the interface to show the new field immediately.
Step 1: The Dynamic Fields Wizard Model
We need a transient model to capture the user's input. This model will essentially act as a "factory" for our custom fields.
import xml.etree.ElementTree as xee
from odoo import api, fields, models, _
class ProjectDynamicFields(models.TransientModel):
_name = 'project.dynamic.fields'
_description = 'Wizard to Create Custom Task Fields'
name = fields.Char(string="Field Name (Technical)", required=True,
help="e.g., x_site_location. Must start with x_")
field_description = fields.Char(string="Label", required=True,
help="The label shown to users, e.g., Site Location")
# Target Model - Fixed to Project Task for this example
model_id = fields.Many2one('ir.model', string='Model', required=True,
default=lambda self: self.env['ir.model'].search([('model', '=', 'project.task')], limit=1))
# Field Type Selection
field_type = fields.Selection([
('char', 'Text'),
('integer', 'Integer'),
('float', 'Decimal'),
('boolean', 'Checkbox'),
('date', 'Date'),
('selection', 'Selection'),
('many2one', 'Many2One')
], string='Field Type', required=True)
# For Relational Fields
ref_model_id = fields.Many2one('ir.model', string='Related Model',
help="Required for Many2One fields")
# Positioning logic
position_field_id = fields.Many2one('ir.model.fields', string='Position Near',
domain="[('model_id', '=', model_id)]", required=True)
position = fields.Selection([('before', 'Before'), ('after', 'After')],
string='Placement', required=True, default='after')
Technical Name
Always prefix with x_ when creating fields from the UI or custom wizards to prevent conflicts with Odoo core updates.
Positioning
We let the user choose an existing field anchor. This gives PMs control over the form layout without touching XML.
Step 2: The Core Logic Function
This method runs when the user clicks "Create Field". It performs two critical database operations transactionally.
def action_create_fields(self):
# 1. Create the Field Record
field_vals = {
'name': self.name,
'field_description': self.field_description,
'model_id': self.model_id.id,
'ttype': self.field_type,
'state': 'manual', # Important: marks it as a custom field
}
# Handle Relational Fields
if self.field_type == 'many2one':
field_vals['relation'] = self.ref_model_id.model
self.env['ir.model.fields'].sudo().create(field_vals)
# 2. Inject into the View
# We create a new extension view that inherits the main Task form
inherit_id = self.env.ref('project.view_task_form2')
arch_base = """
""" % (self.position_field_id.name, self.position, self.name)
self.env['ir.ui.view'].sudo().create({
'name': 'dynamic.task.field.%s' % self.name,
'type': 'form',
'model': 'project.task',
'mode': 'extension',
'inherit_id': inherit_id.id,
'arch_base': arch_base,
'active': True
})
# 3. Reload Page
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
Step 3: The Wizard View
The XML interface for our wizard needs to be intuitive.
<record id="view_project_dynamic_fields_form" model="ir.ui.view">
<field name="name">project.dynamic.fields.form</field>
<field name="model">project.dynamic.fields</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_title">
<h1>Add Custom Task Field</h1>
</div>
<group>
<group string="Definition">
<field name="field_description" placeholder="e.g. Site Location"/>
<field name="name" placeholder="e.g. x_site_location"/>
<field name="field_type"/>
<field name="ref_model_id" invisible="field_type != 'many2one'" required="field_type == 'many2one'"/>
</group>
<group string="Layout">
<field name="position"/>
<field name="position_field_id"/>
</group>
</group>
<footer>
<button name="action_create_fields" string="Generate Field" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</sheet>
</form>
</field>
</record>
Practical Application
Imagine you are managing a Software Development project. Your team needs to track the "Client Browser" for every bug report task.
- Launch Wizard: Open the "Add Task Field" menu item.
- Configure:
- Label: Client Browser
- Technical Name: x_client_browser
- Type: Selection (or Text)
- Position: After "Tags"
- Generate: Click the button. The page reloads.
- Result: Open any Project Task, and you will see the new field ready for data entry immediately.
FAQ
ir.model.fields, you can delete them from the "Database Structure > Fields" menu in Settings. However, deleting a field deletes all data stored in it.
state='manual'. Standard Odoo updates generally respect manual fields unless you do a full database reset.
