Odoo Code Evolution with Developer Standards and Best Practices v10–v18
Odoo has been on a never-ending journey from version 10 to version 18, always pushing for modernization, simplicity and following modern developer standards. This change isn’t just about adding new features; it’s a big change in how developers work with the framework that makes Odoo code cleaner, faster, and much less verbose. As Odoo grew up, it began to get free of old syntax in its Python, XML, and frontend layers (introducing OWL). This made developers to follow best practices that were in line with what was happening in the industry as a whole. The result is a more elegant development experience. Older versions had _columns and verbose conditional XML but modern OWL has the concise chatter/tag, Python’s Command API, and a simpler component architecture this keeps the Odoo framework powerful and useful in the constantly evolving landscape of enterprise software development.
Let’s discuss the significant developments in Odoo’s core: XML Views, OWL, and the Python ORM/API.
XML Changes Across Odoo Versions (v10 → v18)
Renamed to <tree> to <list>
Before Odoo18
<tree>
<field name="name"/>
</tree>
In Odoo18
<list>
<field name="name"/>
</list>
Simplified conditional attributes (attrs, states) replaced by the direct boolean attributes
Before Odoo17
<field name="department_id" attrs="{'invisible': ['|', ('state', '=', 'done'), ('type', '=',
'internal')]}"/>
In Odoo18
<field name="department_id" invisible="state == 'done' or type == 'internal'"/>
Simplified QWeb chatter declaration
Odoo10 → 17 (Verbose)
You had to declare all the chatter fields explicitly:
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
In Odoo18 (Simplified)
<chatter/>
Or with added attributes:
<chatter/>
Cleaner use of the date-range widget
Before Odoo18
<field name="start_date" widget="daterange" options="{'related_end_date': 'end_date'}"/>
<field name="end_date" widget="daterange" options="{'related_start_date': 'start_date'}"/>
Now In Odoo18
<field name="start_date" widget="daterange" options="{'end_date_field': 'end_date'}"/>
Updated the layout structure of res.config.settings
Before Odoo17
<div class="app_settings_block" ...>
<h2>Example Settings</h2>
<div class="row mt16 o_settings_container">
...
</div>
</div>
Now
<app string="Application Settings">
<block title="Example Settings">
<setting string="Example Setting" help="Description...">
<field name="example_setting"/>
</setting>
</block>
</app>
OWL in Odoo: Version Compatibility with Examples (v14 → v18)
Basic Component (No Change)
-> Works the same in v14 → v18.
xml
<t t-name="my_module.MyComponent" owl="1">
<div>
<h3>Hello <t t-esc="props.name"/></h3>
<button t-on-click="sayHello">Click Me</button>
</div>
</t>
js
import { Component } from "@odoo/owl";
export class MyComponent extends Component {
setup() {}
sayHello() {
alert(`Hello ${this.props.name}`);
}
}
MyComponent.template = "my_module.MyComponent";
Reactivity with useState (No Change)
-> Works the same in v14 → v18.
xml
<t t-name="my_module.Counter" owl="1">
<div>
<span>Counter: <t t-esc="state.count"/></span>
<button t-on-click="increment">+</button>
</div>
</t>
js
import { Component, useState } from "@odoo/owl";
export class Counter extends Component {
setup() {
this.state = useState({ count: 0 });
}
increment() {
this.state.count++;
}
}
Counter.template = "my_module.Counter"
Extending / Patching Components
-> Odoo 14–15 (Old Style – extend)
import ListRenderer from "web.ListRenderer";
ListRenderer.include({
setup() {
this._super.apply(this, arguments);
console.log("ListRenderer patched (old way)");
},
});
-> Odoo 16–18 (New Style – patch)
import { patch } from "@web/core/utils/patch";
import { ListRenderer } from "@web/views/list/list_renderer";
patch(ListRenderer.prototype, "my_module.list_patch", {
setup() {
this._super();
console.log("ListRenderer patched (modern way)");
},
});
include/extend was common before now patch() is the recommended approach.
Hooks (New in Odoo18)
-> Odoo14–17 (Manual event handling)
import { Component, useState } from "@odoo/owl";
export class Notifier extends Component {
setup() {
this.state = useState({ message: "Waiting..." });
this.env.bus.on("notification", this, this._onNotification);
}
_onNotification(ev) {
this.state.message = ev.detail;
}
}
Notifier.template = "my_module.Notifier";
-> Odoo18 (Cleaner with useBus Hook)
import { Component, useState } from "@odoo/owl";
import { useBus } from "@web/core/utils/hooks";
export class Notifier extends Component {
setup() {
this.state = useState({ message: "Waiting..." });
useBus(this.env.bus, "notification", (msg) => {
this.state.message = msg.detail;
});
}
}
Notifier.template = "my_module.Notifier";
In v18, hooks like useBus, useAssets, useAutofocus make the Odoo code shorter and safer (auto-cleanup).
Python (ORM/API) Changes Across Odoo Versions
Odoo10 → 11
Before (Odoo10, old API allowed)
from openerp.osv import osv, fields
class Partner(osv.osv):
_name = "res.partner"
_columns = {
'name': fields.char('Name'),
}
After (Odoo11+, new API only)
from odoo import models, fields
class Partner(models.Model):
_name = "res.partner"
name = fields.Char(string="Name")
Odoo12–13
- ORM largely stable.
- @api.multi decorator deprecated → default is always multi-record.
# Odoo11
@api.multi
def action_confirm(self):
for rec in self:
rec.state = "confirm"
#Odoo12+
def action_confirm(self):
for rec in self:
rec.state = "confirm"
Odoo14
x2many Command Tuples
In Odoo 14, the classic tuple syntax ((0, 0, vals), (4, id), (2, id), etc.) for One2many / Many2many fields was still the standard way to update relational fields.At the same time,Odoo’s core team began preparing for modernization (which later became the Command API in Odoo16).
-> Old Style (Tuple Commands – Odoo14)
order = self.env['sale.order'].create({
'partner_id': 1,
'order_line': [
(0, 0, {'product_id': 1, 'product_uom_qty': 2, 'price_unit': 100}),
(0, 0, {'product_id': 2, 'product_uom_qty': 1, 'price_unit': 50}),
]
})
Other tuple commands supported:
- (0, 0, vals) → Create new record
- (1, id, vals) → Update existing record
- (2, id) → Delete record
- (3, id) → Unlink relation only
- (4, id) → Link existing record
- (5,) → Remove all links
- (6, 0, [ids]) → Replace links with given IDs
-> Same Example with Command (Odoo16+)
from odoo import Command
order = self.env['sale.order'].create({
'partner_id': 1,
'order_line': [
Command.create({'product_id': 1, 'product_uom_qty': 2, 'price_unit': 100}),
Command.create({'product_id': 2,'product_uom_qty': 1,'price_unit': 50}),
]
})
Other tuple commands supported:
- Command.create(vals) → Create new record
- Command.update(id, vals) → Update existing record
- Command.delete(id) → Delete record
- Command.unlink(id)→ Unlink relation only
- Command.link(id) → Link existing record
- Command.clear() → Remove all links
- Command.set([ids])→ Replace links with given IDs
These changes make the Odoo code better, easier to maintain, and better for developers. To keep your Odoo apps up to date and in line with these new standards, you need to keep learning and changing.
Want to learn the new Odoo standard to make your apps future-proof? Visit Transines Solutions Youtube today to find in-depth tutorials, advanced development guides, and complete migration plans made just for the newest versions of Odoo.
"Automate Your Business with our Customized Odoo ERP Solutions"
"Get a Cost Estimate for Your ERP Project, Absolutely FREE!"
Get a Free Quote



