Command Class in Odoo 18
By Braincuber Team
Published on January 28, 2026
Before Odoo 14, manipulating One2many and Many2many fields meant writing cryptic tuples: (0, 0, vals) to create, (1, id, vals) to update, (4, id) to link. Nobody could remember which number did what without checking the documentation. The Command class changed everything—now you write Command.create(vals) and the intent is immediately clear.
This tutorial covers every Command method available in Odoo 18. You'll learn when to use each one, see practical examples with Invoice lines and Product Tags, and understand the differences between deleting records versus unlinking them. By the end, you'll write cleaner, more maintainable relational field code.
What You'll Learn: All six Command methods (create, update, delete, link, unlink, clear), the difference between One2many and Many2many operations, legacy tuple syntax vs modern Command class, and real-world examples with Invoice lines and Product categories.
Legacy Tuples vs Command Class
| Operation | Legacy Tuple | Command Class |
|---|---|---|
| Create new record | (0, 0, {'name': 'X'}) | Command.create({'name': 'X'}) |
| Update existing record | (1, id, {'name': 'Y'}) | Command.update(id, {'name': 'Y'}) |
| Delete record | (2, id, 0) | Command.delete(id) |
| Forget record (unlink M2M) | (3, id, 0) | Command.unlink(id) |
| Link existing record | (4, id, 0) | Command.link(id) |
| Clear all records | (5, 0, 0) | Command.clear() |
| Replace all with set | (6, 0, [ids]) | Command.set([ids]) |
Deprecation Notice: While legacy tuples still work, they're discouraged in Odoo 16+. Use Command class for all new development. It's clearer, less error-prone, and future-compatible.
Import the Command Class
from odoo import models, fields, api, Command
Command Methods Explained
Use when adding new records to a One2many or Many2many field. The vals dictionary contains field values for the new record. Works for both field types.
# Add multiple invoice lines to an existing invoice
invoice = self.env['account.move'].browse(invoice_id)
invoice.write({
'invoice_line_ids': [
Command.create({
'product_id': self.env.ref('product.product_product_1').id,
'name': 'Consulting Services',
'quantity': 10,
'price_unit': 150.00,
}),
Command.create({
'product_id': self.env.ref('product.product_product_2').id,
'name': 'Development Hours',
'quantity': 40,
'price_unit': 125.00,
}),
]
})
Modify specific fields on an existing related record. You need the record's ID. Only the fields in vals are updated; others remain unchanged.
# Update price and quantity on existing invoice line
line_id = 42 # ID of the invoice line to update
invoice.write({
'invoice_line_ids': [
Command.update(line_id, {
'quantity': 50,
'price_unit': 175.00,
'discount': 10.0,
}),
]
})
# Update multiple lines at once
invoice.write({
'invoice_line_ids': [
Command.update(42, {'quantity': 50}),
Command.update(43, {'quantity': 25}),
Command.update(44, {'discount': 15.0}),
]
})
Removes the record from database completely. Use for One2many fields where child records belong only to the parent. Cannot be undone.
# Delete specific invoice lines
invoice.write({
'invoice_line_ids': [
Command.delete(42), # Delete line with ID 42
Command.delete(43), # Delete line with ID 43
]
})
# Delete all lines matching a condition
lines_to_delete = invoice.invoice_line_ids.filtered(
lambda l: l.quantity == 0
)
invoice.write({
'invoice_line_ids': [Command.delete(line.id) for line in lines_to_delete]
})
Delete vs Unlink: Command.delete() permanently removes the record from database. For Many2many fields where you just want to remove the association without deleting the record, use Command.unlink() instead.
Creates an association in the pivot table without modifying the record itself. Use for Many2many fields when adding existing records to the relationship.
# Add existing category tags to a product (Many2many)
product = self.env['product.template'].browse(product_id)
# Link single tag
product.write({
'product_tag_ids': [
Command.link(5), # Link tag with ID 5
]
})
# Link multiple tags at once
tag_ids = [5, 12, 18]
product.write({
'product_tag_ids': [Command.link(tag_id) for tag_id in tag_ids]
})
# Add user to security groups
user = self.env['res.users'].browse(user_id)
user.write({
'groups_id': [
Command.link(self.env.ref('sales_team.group_sale_salesman').id),
Command.link(self.env.ref('stock.group_stock_user').id),
]
})
Removes the link in Many2many pivot table. The record itself remains in the database. Use when you want to disconnect records without destroying them.
# Remove specific tags from product (tags still exist in database)
product.write({
'product_tag_ids': [
Command.unlink(5), # Remove association with tag 5
Command.unlink(12), # Remove association with tag 12
]
})
# Remove user from security group
user.write({
'groups_id': [
Command.unlink(self.env.ref('sales_team.group_sale_manager').id),
]
})
For One2many: deletes all child records. For Many2many: removes all associations (records remain). Use to reset the relational field.
# Clear all invoice lines (deletes them from database)
invoice.write({
'invoice_line_ids': [Command.clear()]
})
# Clear all product tags (removes associations, tags remain)
product.write({
'product_tag_ids': [Command.clear()]
})
# Clear then add new records in single operation
product.write({
'product_tag_ids': [
Command.clear(),
Command.link(new_tag_1.id),
Command.link(new_tag_2.id),
]
})
Completely replaces the current set of linked records with the provided list. Equivalent to clear() then link() for each ID. For Many2many fields primarily.
# Replace all product tags with exact set
product.write({
'product_tag_ids': [Command.set([5, 12, 18])]
})
# Set user groups to specific list
user.write({
'groups_id': [Command.set([
self.env.ref('base.group_user').id,
self.env.ref('sales_team.group_sale_salesman').id,
])]
})
Combining Multiple Commands
from odoo import Command
def update_invoice_lines(self, invoice):
"""
Single write() with multiple operations:
- Delete cancelled lines
- Update pricing on existing lines
- Add new service line
"""
invoice.write({
'invoice_line_ids': [
# Delete lines with zero quantity
Command.delete(42),
Command.delete(43),
# Update existing line prices
Command.update(44, {'price_unit': 200.00, 'discount': 5.0}),
Command.update(45, {'quantity': 100}),
# Add new line
Command.create({
'product_id': self.env.ref('product.product_delivery').id,
'name': 'Express Shipping',
'quantity': 1,
'price_unit': 25.00,
}),
]
})
One2many vs Many2many Behavior
One2many Fields
create()- Creates child record linked to parentupdate()- Updates child recorddelete()- Permanently deletes child recordclear()- Deletes ALL child recordslink()- Rarely used (reassigns existing record)
Many2many Fields
create()- Creates AND links new recordlink()- Adds existing record to relationunlink()- Removes from relation (record kept)clear()- Removes all associationsset()- Replaces with exact list of IDs
Conclusion
The Command class transforms cryptic tuple syntax into readable, self-documenting code. Instead of remembering that (4, id) means "link", you write Command.link(id)—the intent is obvious. For One2many fields, use create/update/delete. For Many2many fields, link and unlink manage associations without destroying records. Combine multiple commands in a single write() for atomic operations.
Key Takeaways: Import from odoo module. Use delete() for One2many (destroys record), unlink() for Many2many (keeps record). Combine commands in one write() call. Replace legacy tuples in existing code.
