Skip to content

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'
    )
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 %')
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'
    )