Skip to Content

17 - Onchange Mechanisms (@api.onchange)

Welcome back! In our last lesson, we learned how to use Computed Fields to automatically calculate data based on other fields. That is great for math and logic that needs to be permanently tied to your records.

But what if you just want to update the screen to help the user while they are typing, before they even click "Save"?

Imagine a user selecting a "Product" from a drop-down menu on an invoice. The moment they select it, you want the "Unit Price" and "Description" fields to automatically fill in. This is about User Experience (UX). For this, we use the Onchange Mechanism.


1. What is @api.onchange?

An onchange method is a Python function that is triggered instantly by the user's browser whenever they change a specific field on a form.

When the user changes a field, the browser sends a quick message to the Odoo server with the unsaved form data. The server runs your @api.onchange function, updates the values, and sends them back to the browser to display on the screen. Nothing is saved to the database yet.

Because this happens entirely on the fly, it is perfect for setting default values based on user choices or warning the user about mistakes.


2. Writing Your First Onchange Method

Let's build a classic example. We have a simple order line model. When the user selects a product, we want to automatically pull that product's price into our order line.

from odoo import models, fields, api

class SimpleOrderLine(models.Model):
_name = 'simple.order.line'
_description = 'Simple Order Line'

product_id = fields.Many2one('product.product', string='Product')
quantity = fields.Integer(string='Quantity', default=1)
price_unit = fields.Float(string='Unit Price')
description = fields.Char(string='Description')

# We tell Odoo to watch the 'product_id' field for any changes
@api.onchange('product_id')
def _onchange_product_id(self):
# We don't need a loop here, because onchange only ever runs on a single
# unsaved record in the form view!
if self.product_id:
# When a product is selected, we update the price and description
self.price_unit = self.product_id.list_price
self.description = self.product_id.name
else:
# If the user clears the product, we clear the price and description
self.price_unit = 0.0
self.description = ''

Now, the moment a user selects a product from the drop-down, the price and description appear like magic! The user can still click into the price_unit field and manually change it if they want to give a special discount, because it is just a regular field, not a locked computed field.


3. Returning User Warnings

One of the most powerful features of @api.onchange is its ability to pop up warning messages on the user's screen without stopping them from working or crashing the system with a hard error.

Let's say a user types in an order quantity. We want to check our inventory, and if they order more than we have, we just want to politely warn them.

@api.onchange('quantity', 'product_id')
def _onchange_check_inventory(self):
if self.product_id and self.quantity > self.product_id.qty_available:
# To show a warning, an onchange method must return a specific dictionary
return {
'warning': {
'title': 'Low Inventory Warning!',
'message': f'You are ordering {self.quantity}, but we only have {self.product_id.qty_available} in stock.'
}
}

When the user types 50 into the quantity box, a nice pop-up window will appear with your message. They can close it and keep working.


4. Compute vs. Onchange: The Golden Rules

Students often get confused about when to use @api.depends (Compute) and when to use @api.onchange. Here is the developer cheat sheet:

  • Use Compute (@api.depends) when: You are calculating a final result that should always be strictly true (like Total = Quantity * Price). You want the field to be read-only by default.

  • Use Onchange (@api.onchange) when: You just want to set a default value to help the user, but you still want the user to be able to manually overwrite it (like pulling in a standard price). Or, when you need to pop up a UI warning.


💡 Developer Pro-Tips for Odoo 19

  1. The NewId Trap: Inside an onchange method, the record hasn't been saved to the database yet. Because of this, self.id does not exist. Instead, Odoo assigns it a temporary fake ID called NewId. Never try to write database queries or use ORM methods like search() using self.id inside an onchange!

  2. Odoo 19 Best Practice: In modern Odoo development (especially Odoo 19), the core team recommends using Computed Fields (with readonly=False) instead of onchange whenever possible. Compute fields are generally faster and more reliable on the backend. Reserve onchange strictly for UI warnings and complex dynamic default filling.

  3. No Loops Needed: Notice we didn't use for record in self:? That's because onchange is triggered by a user looking at exactly one record on their screen. self will always be a single record here.

Homework: Add an onchange method to your custom model. If the user types a negative number into a price field, have the onchange automatically flip it to 0.0 and return a warning pop-up that says "Price cannot be negative!"

Rating
0 0

There are no comments for now.

to be the first to leave a comment.