Computed Fields
Basic Computed Fields
Simple Computed Field
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('order_line.price_subtotal')
def _compute_total_amount(self):
for record in self:
record.total_amount = sum(record.order_line.mapped('price_subtotal'))
total_amount = fields.Float(
string='Total Amount',
compute='_compute_total_amount',
help='Sum of all order lines'
)
Computed Field with Store
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('order_line.price_total', 'order_line.tax_id')
def _compute_amount_all(self):
for order in self:
amount_untaxed = amount_tax = 0.0
for line in order.order_line:
amount_untaxed += line.price_subtotal
amount_tax += line.price_tax
order.amount_untaxed = amount_untaxed
order.amount_tax = amount_tax
order.amount_total = amount_untaxed + amount_tax
amount_untaxed = fields.Float(
string='Untaxed Amount',
compute='_compute_amount_all',
store=True, # Stored in database for performance
tracking=True
)
amount_tax = fields.Float(
string='Tax Amount',
compute='_compute_amount_all',
store=True
)
amount_total = fields.Float(
string='Total Amount',
compute='_compute_amount_all',
store=True
)
Computed Field with Default
python
class Product(models.Model):
_name = 'product.product'
@api.depends('list_price', 'standard_price')
def _compute_margin(self):
for product in self:
if product.list_price:
product.margin = ((product.list_price - product.standard_price) / product.list_price) * 100
else:
product.margin = 0.0
margin = fields.Float(
string='Margin %',
compute='_compute_margin',
store=True,
default=0.0,
help='Profit margin percentage'
)
Dependencies and Triggers
Simple Dependencies
python
class SaleOrderLine(models.Model):
_name = 'sale.order.line'
@api.depends('product_uom_qty', 'price_unit', 'discount')
def _compute_price_subtotal(self):
for line in self:
price = line.price_unit * (1 - (line.discount / 100.0))
line.price_subtotal = line.product_uom_qty * price
price_subtotal = fields.Float(
compute='_compute_price_subtotal',
string='Subtotal',
store=True
)
Nested Dependencies
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('order_line.price_subtotal', 'order_line.tax_id.amount')
def _compute_tax_totals(self):
for order in self:
tax_totals = {}
for line in order.order_line:
for tax in line.tax_id:
tax_amount = line.price_subtotal * (tax.amount / 100)
if tax.id not in tax_totals:
tax_totals[tax.id] = {
'name': tax.name,
'amount': 0.0
}
tax_totals[tax.id]['amount'] += tax_amount
order.tax_breakdown = tax_totals
tax_breakdown = fields.Text(
compute='_compute_tax_totals',
string='Tax Breakdown'
)
Related Field Dependencies
python
class SaleOrderLine(models.Model):
_name = 'sale.order.line'
@api.depends('order_id.partner_id.country_id')
def _compute_is_domestic(self):
for line in self:
company_country = line.order_id.company_id.country_id
partner_country = line.order_id.partner_id.country_id
line.is_domestic = company_country == partner_country
is_domestic = fields.Boolean(
compute='_compute_is_domestic',
string='Domestic Sale',
store=True
)
Dynamic Dependencies
python
class ProjectTask(models.Model):
_name = 'project.task'
@api.depends('timesheet_ids.unit_amount', 'planned_hours')
def _compute_progress(self):
for task in self:
if task.planned_hours:
spent_hours = sum(task.timesheet_ids.mapped('unit_amount'))
task.progress = min(100, (spent_hours / task.planned_hours) * 100)
else:
task.progress = 0.0
progress = fields.Float(
compute='_compute_progress',
string='Progress %',
store=True
)
@api.depends('stage_id', 'progress')
def _compute_kanban_state(self):
for task in self:
if task.stage_id.fold:
task.kanban_state = 'done'
elif task.progress >= 100:
task.kanban_state = 'done'
elif task.progress > 0:
task.kanban_state = 'in_progress'
else:
task.kanban_state = 'normal'
kanban_state = fields.Selection([
('normal', 'Normal'),
('in_progress', 'In Progress'),
('done', 'Done'),
('blocked', 'Blocked')
], compute='_compute_kanban_state', store=True)
Advanced Computed Field Patterns
Computed Field with Inverse
python
class Product(models.Model):
_name = 'product.product'
@api.depends('list_price', 'discount_percentage')
def _compute_discounted_price(self):
for product in self:
if product.list_price and product.discount_percentage:
product.discounted_price = product.list_price * (1 - product.discount_percentage / 100)
else:
product.discounted_price = product.list_price
def _inverse_discounted_price(self):
for product in self:
if product.list_price and product.discounted_price:
product.discount_percentage = ((product.list_price - product.discounted_price) / product.list_price) * 100
discounted_price = fields.Float(
string='Discounted Price',
compute='_compute_discounted_price',
inverse='_inverse_discounted_price',
store=True
)
discount_percentage = fields.Float(string='Discount %')
Computed Field with Search
python
class ResPartner(models.Model):
_name = 'res.partner'
@api.depends('name', 'email', 'phone')
def _compute_display_info(self):
for partner in self:
info_parts = [partner.name or '']
if partner.email:
info_parts.append(f"({partner.email})")
if partner.phone:
info_parts.append(f"[{partner.phone}]")
partner.display_info = ' '.join(info_parts)
def _search_display_info(self, operator, value):
if operator == 'ilike':
return [
'|', '|',
('name', operator, value),
('email', operator, value),
('phone', operator, value)
]
return []
display_info = fields.Char(
string='Display Information',
compute='_compute_display_info',
search='_search_display_info',
store=False # Not stored, but searchable
)
Multi-Model Computed Fields
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('order_line.product_id.categ_id')
def _compute_product_categories(self):
for order in self:
categories = order.order_line.mapped('product_id.categ_id')
order.product_category_ids = [(6, 0, categories.ids)]
order.category_count = len(categories)
product_category_ids = fields.Many2many(
'product.category',
compute='_compute_product_categories',
string='Product Categories',
store=True
)
category_count = fields.Integer(
compute='_compute_product_categories',
string='Category Count',
store=True
)
Computed Field with External API
python
class ResPartner(models.Model):
_name = 'res.partner'
@api.depends('vat', 'country_id')
def _compute_vat_verification(self):
for partner in self:
if partner.vat and partner.country_id:
# Simulate external VAT verification
partner.vat_verified = self._verify_vat_external(partner.vat, partner.country_id.code)
partner.vat_verification_date = fields.Datetime.now()
else:
partner.vat_verified = False
partner.vat_verification_date = False
def _verify_vat_external(self, vat, country_code):
"""Simulate external VAT verification service"""
# In real implementation, this would call external service
import time
time.sleep(0.1) # Simulate API delay
return len(vat) >= 8 # Simplified validation
vat_verified = fields.Boolean(
compute='_compute_vat_verification',
string='VAT Verified',
store=True
)
vat_verification_date = fields.Datetime(
compute='_compute_vat_verification',
string='Verification Date',
store=True
)
Performance Optimization
Batch Processing
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('order_line.price_subtotal')
def _compute_amount_statistics(self):
# Batch process to avoid N+1 queries
for order in self:
lines = order.order_line
if lines:
order.min_line_amount = min(lines.mapped('price_subtotal'))
order.max_line_amount = max(lines.mapped('price_subtotal'))
order.avg_line_amount = sum(lines.mapped('price_subtotal')) / len(lines)
else:
order.min_line_amount = 0.0
order.max_line_amount = 0.0
order.avg_line_amount = 0.0
min_line_amount = fields.Float(compute='_compute_amount_statistics', store=True)
max_line_amount = fields.Float(compute='_compute_amount_statistics', store=True)
avg_line_amount = fields.Float(compute='_compute_amount_statistics', store=True)
Optimized SQL Queries
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('order_line.price_subtotal')
def _compute_line_statistics(self):
# Use raw SQL for better performance on large datasets
if not self.ids:
return
query = """
SELECT
order_id,
COUNT(*) as line_count,
SUM(price_subtotal) as total_amount,
AVG(price_subtotal) as avg_amount
FROM sale_order_line
WHERE order_id IN %s
GROUP BY order_id
"""
self.env.cr.execute(query, (tuple(self.ids),))
results = {row[0]: row[1:] for row in self.env.cr.fetchall()}
for order in self:
stats = results.get(order.id, (0, 0.0, 0.0))
order.line_count = stats[0]
order.computed_total = stats[1]
order.computed_average = stats[2]
line_count = fields.Integer(compute='_compute_line_statistics', store=True)
computed_total = fields.Float(compute='_compute_line_statistics', store=True)
computed_average = fields.Float(compute='_compute_line_statistics', store=True)
Selective Computation
python
class ProductProduct(models.Model):
_name = 'product.product'
@api.depends('move_ids.product_qty', 'move_ids.state')
def _compute_stock_levels(self):
# Only compute for products that have moves
products_with_moves = self.filtered(lambda p: p.move_ids)
for product in products_with_moves:
incoming_qty = sum(product.move_ids.filtered(
lambda m: m.state in ['waiting', 'confirmed', 'assigned'] and m.location_dest_id.usage == 'internal'
).mapped('product_qty'))
outgoing_qty = sum(product.move_ids.filtered(
lambda m: m.state in ['waiting', 'confirmed', 'assigned'] and m.location_id.usage == 'internal'
).mapped('product_qty'))
product.virtual_available = product.qty_available + incoming_qty - outgoing_qty
# Set zero for products without moves
(self - products_with_moves).virtual_available = 0.0
virtual_available = fields.Float(
compute='_compute_stock_levels',
string='Virtual Available',
store=True
)
Conditional Computed Fields
State-Based Computation
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('state', 'order_line.price_subtotal', 'discount_amount')
def _compute_final_amount(self):
for order in self:
if order.state in ['draft', 'sent']:
# Include discount in draft/sent states
order.final_amount = sum(order.order_line.mapped('price_subtotal')) - order.discount_amount
elif order.state == 'sale':
# No discount after confirmation
order.final_amount = sum(order.order_line.mapped('price_subtotal'))
else:
order.final_amount = 0.0
final_amount = fields.Float(
compute='_compute_final_amount',
string='Final Amount',
store=True
)
User-Context Dependent
python
class ProjectTask(models.Model):
_name = 'project.task'
@api.depends('user_id', 'stage_id')
def _compute_user_can_edit(self):
for task in self:
current_user = self.env.user
if current_user.has_group('project.group_project_manager'):
task.user_can_edit = True
elif task.user_id == current_user and task.stage_id.fold == False:
task.user_can_edit = True
else:
task.user_can_edit = False
user_can_edit = fields.Boolean(
compute='_compute_user_can_edit',
string='User Can Edit'
)
Date-Based Computation
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('date_order', 'commitment_date')
def _compute_delivery_status(self):
today = fields.Date.context_today(self)
for order in self:
if not order.commitment_date:
order.delivery_status = 'no_date'
elif order.commitment_date < today:
order.delivery_status = 'overdue'
elif order.commitment_date == today:
order.delivery_status = 'today'
elif order.commitment_date <= today + timedelta(days=7):
order.delivery_status = 'this_week'
else:
order.delivery_status = 'future'
delivery_status = fields.Selection([
('no_date', 'No Date Set'),
('overdue', 'Overdue'),
('today', 'Due Today'),
('this_week', 'Due This Week'),
('future', 'Future')
], compute='_compute_delivery_status', store=True)
Computed Fields with Complex Logic
Recursive Computation
python
class ProductCategory(models.Model):
_name = 'product.category'
@api.depends('parent_id', 'parent_id.complete_name')
def _compute_complete_name(self):
for category in self:
if category.parent_id:
category.complete_name = f"{category.parent_id.complete_name} / {category.name}"
else:
category.complete_name = category.name
complete_name = fields.Char(
compute='_compute_complete_name',
string='Complete Name',
store=True,
recursive=True
)
Hierarchical Computation
python
class HrEmployee(models.Model):
_name = 'hr.employee'
@api.depends('child_ids', 'child_ids.subordinate_count')
def _compute_subordinate_count(self):
for employee in self:
# Direct subordinates
direct_count = len(employee.child_ids)
# Indirect subordinates (recursive)
indirect_count = sum(employee.child_ids.mapped('subordinate_count'))
employee.subordinate_count = direct_count + indirect_count
subordinate_count = fields.Integer(
compute='_compute_subordinate_count',
string='Total Subordinates',
store=True
)
Aggregation Across Models
python
class ResPartner(models.Model):
_name = 'res.partner'
@api.depends('sale_order_ids.amount_total', 'sale_order_ids.state')
def _compute_sales_statistics(self):
for partner in self:
confirmed_orders = partner.sale_order_ids.filtered(lambda o: o.state == 'sale')
draft_orders = partner.sale_order_ids.filtered(lambda o: o.state == 'draft')
partner.total_sales = sum(confirmed_orders.mapped('amount_total'))
partner.potential_sales = sum(draft_orders.mapped('amount_total'))
partner.order_count = len(confirmed_orders)
partner.avg_order_value = partner.total_sales / len(confirmed_orders) if confirmed_orders else 0
total_sales = fields.Float(compute='_compute_sales_statistics', store=True)
potential_sales = fields.Float(compute='_compute_sales_statistics', store=True)
order_count = fields.Integer(compute='_compute_sales_statistics', store=True)
avg_order_value = fields.Float(compute='_compute_sales_statistics', store=True)
Error Handling in Computed Fields
Safe Computation with Try-Catch
python
class ProductProduct(models.Model):
_name = 'product.product'
@api.depends('standard_price', 'list_price')
def _compute_profit_margin(self):
for product in self:
try:
if product.standard_price and product.list_price:
product.profit_margin = ((product.list_price - product.standard_price) / product.list_price) * 100
else:
product.profit_margin = 0.0
except (ZeroDivisionError, TypeError):
product.profit_margin = 0.0
_logger.warning(f"Error computing profit margin for product {product.id}")
profit_margin = fields.Float(
compute='_compute_profit_margin',
string='Profit Margin %',
store=True,
help='Profit margin percentage (safe computation)'
)
Validation in Computed Fields
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('order_line.product_id', 'order_line.product_uom_qty')
def _compute_order_validation(self):
for order in self:
issues = []
# Check for duplicate products
products = order.order_line.mapped('product_id')
if len(products) != len(order.order_line):
issues.append('Duplicate products found')
# Check for zero quantities
if any(line.product_uom_qty <= 0 for line in order.order_line):
issues.append('Zero or negative quantities found')
# Check for missing products
if any(not line.product_id for line in order.order_line):
issues.append('Missing products in order lines')
order.validation_issues = '; '.join(issues) if issues else False
order.is_valid = not bool(issues)
validation_issues = fields.Text(
compute='_compute_order_validation',
string='Validation Issues'
)
is_valid = fields.Boolean(
compute='_compute_order_validation',
string='Order Valid'
)
Computed Fields in Different Contexts
Portal/Website Computed Fields
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('order_line', 'state', 'partner_id')
def _compute_portal_display(self):
for order in self:
# Compute display information for portal
if order.state == 'draft':
order.portal_status = 'Quotation'
order.portal_status_class = 'info'
elif order.state == 'sale':
order.portal_status = 'Confirmed'
order.portal_status_class = 'success'
else:
order.portal_status = 'Other'
order.portal_status_class = 'secondary'
# Compute summary for portal
order.portal_summary = f"{len(order.order_line)} items, Total: {order.amount_total}"
portal_status = fields.Char(compute='_compute_portal_display')
portal_status_class = fields.Char(compute='_compute_portal_display')
portal_summary = fields.Char(compute='_compute_portal_display')
Reporting Computed Fields
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('date_order', 'amount_total')
def _compute_reporting_fields(self):
for order in self:
if order.date_order:
order.year = order.date_order.year
order.month = order.date_order.month
order.quarter = (order.date_order.month - 1) // 3 + 1
order.week = order.date_order.isocalendar()[1]
# Categorize amounts for reporting
if order.amount_total >= 10000:
order.amount_category = 'large'
elif order.amount_total >= 1000:
order.amount_category = 'medium'
else:
order.amount_category = 'small'
year = fields.Integer(compute='_compute_reporting_fields', store=True)
month = fields.Integer(compute='_compute_reporting_fields', store=True)
quarter = fields.Integer(compute='_compute_reporting_fields', store=True)
week = fields.Integer(compute='_compute_reporting_fields', store=True)
amount_category = fields.Selection([
('small', 'Small'),
('medium', 'Medium'),
('large', 'Large')
], compute='_compute_reporting_fields', store=True)
Testing Computed Fields
Unit Tests for Computed Fields
python
from odoo.tests.common import TransactionCase
class TestComputedFields(TransactionCase):
def setUp(self):
super().setUp()
self.partner = self.env['res.partner'].create({'name': 'Test Partner'})
self.product = self.env['product.product'].create({
'name': 'Test Product',
'list_price': 100.0,
'standard_price': 80.0
})
def test_computed_field_calculation(self):
"""Test basic computed field calculation"""
order = self.env['sale.order'].create({
'partner_id': self.partner.id,
})
# Create order line
self.env['sale.order.line'].create({
'order_id': order.id,
'product_id': self.product.id,
'product_uom_qty': 2,
'price_unit': 100.0
})
# Test computed field
self.assertEqual(order.amount_total, 200.0)
def test_computed_field_dependencies(self):
"""Test computed field dependency updates"""
order = self.env['sale.order'].create({
'partner_id': self.partner.id,
})
line = self.env['sale.order.line'].create({
'order_id': order.id,
'product_id': self.product.id,
'product_uom_qty': 1,
'price_unit': 100.0
})
initial_total = order.amount_total
# Update dependency
line.product_uom_qty = 3
# Verify recomputation
self.assertNotEqual(order.amount_total, initial_total)
self.assertEqual(order.amount_total, 300.0)
def test_computed_field_performance(self):
"""Test computed field performance"""
import time
# Create multiple orders
orders = self.env['sale.order']
for i in range(100):
order = self.env['sale.order'].create({
'partner_id': self.partner.id,
})
self.env['sale.order.line'].create({
'order_id': order.id,
'product_id': self.product.id,
'product_uom_qty': 1,
'price_unit': 100.0
})
orders |= order
# Test batch computation performance
start_time = time.time()
totals = orders.mapped('amount_total')
end_time = time.time()
self.assertLess(end_time - start_time, 1.0) # Should complete within 1 second
self.assertEqual(len(totals), 100)
Best Practices and Common Patterns
Efficient Dependency Declaration
python
class SaleOrder(models.Model):
_name = 'sale.order'
# Good: Specific dependencies
@api.depends('order_line.price_subtotal', 'order_line.discount')
def _compute_amount_total(self):
for order in self:
order.amount_total = sum(
line.price_subtotal * (1 - line.discount / 100)
for line in order.order_line
)
# Avoid: Overly broad dependencies
# @api.depends('order_line') # This would trigger on ANY order_line change
amount_total = fields.Float(compute='_compute_amount_total', store=True)
Memory-Efficient Computation
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('order_line.price_subtotal')
def _compute_statistics(self):
for order in self:
# Use generator expressions for memory efficiency
subtotals = (line.price_subtotal for line in order.order_line)
subtotal_list = list(subtotals) # Convert to list once
if subtotal_list:
order.min_line_amount = min(subtotal_list)
order.max_line_amount = max(subtotal_list)
order.total_amount = sum(subtotal_list)
order.avg_line_amount = order.total_amount / len(subtotal_list)
else:
order.min_line_amount = 0.0
order.max_line_amount = 0.0
order.total_amount = 0.0
order.avg_line_amount = 0.0
min_line_amount = fields.Float(compute='_compute_statistics', store=True)
max_line_amount = fields.Float(compute='_compute_statistics', store=True)
total_amount = fields.Float(compute='_compute_statistics', store=True)
avg_line_amount = fields.Float(compute='_compute_statistics', store=True)
Documentation and Naming
python
class SaleOrder(models.Model):
_name = 'sale.order'
@api.depends('order_line.price_subtotal', 'global_discount_percent')
def _compute_amounts_with_discount(self):
"""
Compute order amounts including global discount.
This method calculates:
- Base amount (sum of line subtotals)
- Global discount amount
- Final amount after discount
The computation is triggered when:
- Order lines are added/removed/modified
- Global discount percentage changes
"""
for order in self:
base_amount = sum(order.order_line.mapped('price_subtotal'))
discount_amount = base_amount * (order.global_discount_percent / 100)
order.base_amount = base_amount
order.discount_amount = discount_amount
order.final_amount = base_amount - discount_amount
base_amount = fields.Float(
compute='_compute_amounts_with_discount',
string='Base Amount',
store=True,
help='Sum of all order line subtotals before global discount'
)
discount_amount = fields.Float(
compute='_compute_amounts_with_discount',
string='Discount Amount',
store=True,
help='Amount of global discount applied'
)
final_amount = fields.Float(
compute='_compute_amounts_with_discount',
string='Final Amount',
store=True,
help='Final amount after applying global discount'
)