Odoo Code Evolution with Developer Standards and Best Practices v10–v18

SAVAD
September 27, 2025
odoo-code

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

Leave a Reply

Your email address will not be published. Required fields are marked *