19 - Overriding CRUD Methods (create, write, unlink)
Welcome back! Up until now, we have let Odoo do a lot of the heavy lifting. When a user clicks "Save" on a new record, Odoo automatically creates a row in the database. When they edit a field, Odoo updates it. When they click "Delete", Odoo removes it.
This automatic process is called CRUD (Create, Read, Update, Delete).
But what if you need to intercept that process? What if you want to automatically generate a unique sequence number right before a record is created? Or what if you want to absolutely forbid a user from deleting an Invoice if it has already been paid?
To do this, we have to intercept Odoo's default behavior by overriding the core ORM methods: create, write, and unlink.
Let's learn how to take total control.
1. The Magic of super()
Before we dive into the specific methods, you must understand one golden rule: We never want to completely replace Odoo's core methods; we just want to add our own logic to them.
If you write a custom create method and forget to actually tell the database to save the data, your app will break. To add our logic and keep Odoo's default database saving behavior, we use the Python super() function.
Think of super() as saying: "Run my custom code first, and then pass the baton back to Odoo to finish the standard job."
2. Overriding create (Making New Records)
The create method is called the exact moment a new record is being saved to the database.
In Odoo 19, the system is highly optimized. It doesn't create records one by one; it creates them in batches to speed up the database. Because of this, Odoo passes a list of dictionaries called vals_list (values list) to the create method. We must use the @api.model_create_multi decorator.
Real-World Scenario: Let's automatically assign a unique ID (like 'REQ-001') to a Support Request when it gets created.
from odoo import models, fields, api
class SupportRequest(models.Model):
_name = 'support.request'
_description = 'Customer Support Request'
# Defaults to 'New' until we override the create method!
name = fields.Char(string='Request ID', default='New', readonly=True)
customer_name = fields.Char(string='Customer', required=True)
issue_description = fields.Text(string='Issue')
@api.model_create_multi
def create(self, vals_list):
# 1. Add our custom logic BEFORE saving
for vals in vals_list:
# Check if the name is still the default 'New'
if vals.get('name', 'New') == 'New':
# Fetch the next sequence number from Odoo's sequence engine
vals['name'] = self.env['ir.sequence'].next_by_code('support.request.seq') or 'New'
# 2. Pass the modified vals_list back to Odoo to actually create the records
records = super().create(vals_list)
# 3. (Optional) You can also add logic AFTER the record is created!
# For example, sending a welcome email right here using the 'records' variable.
return records
3. Overriding write (Updating Existing Records)
The write method is called whenever an existing record is modified and saved. Unlike create, write receives a single dictionary called vals containing only the fields that were changed.
Real-World Scenario: Let's keep a history log. If a user changes the status of an order to 'Shipped', we want to lock the delivery address so nobody can change it afterward.
from odoo import models, fields
from odoo.exceptions import UserError
class DeliveryOrder(models.Model):
_name = 'delivery.order'
_description = 'Delivery Order'
state = fields.Selection([('draft', 'Draft'), ('shipped', 'Shipped')], default='draft')
delivery_address = fields.Char(string='Address')
def write(self, vals):
# 1. Custom logic BEFORE updating
# Check if the user is trying to change the address
if 'delivery_address' in vals:
for record in self:
# If the order is already shipped, block the change!
if record.state == 'shipped':
raise UserError("You cannot change the address of an order that has already shipped!")
# 2. Pass the baton back to Odoo to save the changes
return super().write(vals)
4. Overriding unlink (Deleting Records)
The unlink method is called when a user selects a record and clicks "Delete". It does not receive any vals because we aren't writing data; we are destroying it.
Real-World Scenario: We want to absolutely forbid users from deleting an invoice unless it is in the 'Draft' state.
from odoo import models, fields
from odoo.exceptions import ValidationError
class CustomerInvoice(models.Model):
_name = 'customer.invoice'
_description = 'Customer Invoice'
customer_id = fields.Many2one('res.partner', string='Customer')
state = fields.Selection([
('draft', 'Draft'),
('posted', 'Posted'),
('paid', 'Paid')
], default='draft')
def unlink(self):
# 1. Custom logic BEFORE deletion
for record in self:
if record.state != 'draft':
# Stop the deletion process entirely with a ValidationError
raise ValidationError("Critical Error: You can only delete invoices that are in the Draft state!")
# 2. If the check passes, tell Odoo to permanently delete the records
return super().unlink()
💡 Developer Pro-Tips for Odoo 19
Never Forget the Return: The most common mistake junior developers make is forgetting to write return super()... at the end of their override. If you forget this, Odoo will literally stop saving or updating your data, and your app will silently fail!
vals Only Contains Changes: In a write method, vals is a dictionary of what the user just changed. If the user only updated the state, then vals will look like {'state': 'shipped'}. It will not contain the delivery_address. If you need to check the current value of a field that wasn't just changed, use record.delivery_address.
Use Constraints First: Before you override write or create just to validate data, ask yourself: "Could I do this with an @api.constrains method instead?" Using Python constraints is cleaner for pure validation. Overriding CRUD methods is best reserved for when you actually need to modify data right before it saves, or block deletions.
Homework: Try overriding the unlink method on a custom model so that only users who belong to a specific administrative group can delete records. You can check a user's group using self.env.user.has_group('base.group_system').
There are no comments for now.