Skip to content

Understanding Odoo Environment, Context, and Company: Developer Deep Dive

DeployMonkey Team · March 23, 2026 12 min read

The Odoo Environment (env)

The env object is the gateway to everything in Odoo's ORM. Every model, every recordset, every method call happens through an environment. Understanding env is fundamental to Odoo development.

# Accessing env from a model method
def action_process(self):
    env = self.env
    env.uid           # current user ID
    env.user          # current user recordset (res.users)
    env.company       # current company recordset
    env.companies     # all companies the user has access to
    env.cr            # database cursor
    env.context       # context dictionary
    env.su            # True if running in sudo mode
    env.lang          # current language code
    env.ref('xml_id') # resolve XML ID to record

Accessing Models via env

# Access any model
partners = self.env['res.partner'].search([])
products = self.env['product.product'].browse([1, 2, 3])

# Create records
new_record = self.env['my.model'].create({'name': 'Test'})

# Access current user's partner
partner = self.env.user.partner_id

The Context Dictionary

The context is an immutable dictionary that flows through ORM operations, affecting behavior at every level:

# Reading context
lang = self.env.context.get('lang', 'en_US')
tz = self.env.context.get('tz', 'UTC')
active_id = self.env.context.get('active_id')
active_ids = self.env.context.get('active_ids', [])

# Default values from context
# When creating records, fields check for 'default_fieldname' in context
# context = {'default_partner_id': 5}
# -> new record gets partner_id = 5

Modifying Context with with_context()

Since context is immutable, you create a new environment with modified context:

# Add to existing context
records = self.with_context(custom_flag=True).search([])

# Replace entire context
records = self.with_context({'lang': 'fr_FR'}).search([])

# Common patterns
# Force language for translations
name_fr = product.with_context(lang='fr_FR').name

# Set defaults for record creation
new_line = self.env['order.line'].with_context(
    default_order_id=order.id,
    default_currency_id=order.currency_id.id,
).create({'product_id': product.id})

# Pass data to computed fields
records = self.with_context(
    date_from=start_date,
    date_to=end_date,
).mapped('computed_balance')

Important Context Keys

KeyPurpose
langLanguage for translations
tzTimezone for date display
uidCurrent user ID
active_idID of the record that triggered the action
active_idsIDs of selected records (multi-select)
active_modelModel of the triggering record
default_*Default values for new records
search_default_*Pre-activate search filters
group_byDefault grouping
tracking_disableDisable mail tracking
mail_create_nologSkip creation log message
force_companyOverride company context

Company Context

Odoo's multi-company system uses the environment to determine which company's data to access:

# Current company
company = self.env.company  # res.company recordset

# All allowed companies
companies = self.env.companies  # recordset of all user's companies

# Switch company context
def _process_for_company(self, company):
    return self.with_company(company)._compute_values()

# with_company changes both env.company and allowed companies
record = self.with_company(other_company)
record.env.company  # other_company

with_user()

Switch the current user:

# Switch to a specific user
record_as_admin = self.with_user(admin_user)

# Switch to superuser (bypasses access rights)
record_su = self.with_user(SUPERUSER_ID)

# Equivalent to sudo() but more explicit
record_sudo = self.sudo()

Context in Computed Fields

Context flows into computed field methods, which can cause subtle bugs:

# This computed field depends on context!
@api.depends_context('date_from')
def _compute_balance(self):
    date_from = self.env.context.get('date_from')
    for record in self:
        if date_from:
            record.balance = record._get_balance_at(date_from)
        else:
            record.balance = record._get_current_balance()

Use @api.depends_context('key') to declare that a computed field depends on a context key. Without this, Odoo may cache the result from a different context.

Context in XML Views

<!-- Set context on action -->
<record id="action_tickets" model="ir.actions.act_window">
  <field name="context">{
    'default_priority': '2',
    'search_default_filter_my': 1,
    'group_by': 'state',
  }</field>
</record>

<!-- Set context on a relational field -->
<field name="line_ids"
       context="{'default_date': date_order,
                 'default_partner_id': partner_id}"/>

<!-- Set context on a button -->
<button name="action_process" type="object"
        context="{'process_mode': 'quick'}"/>

env.ref() for XML ID Resolution

# Resolve an XML ID to a record
template = self.env.ref('module.email_template_id')
group = self.env.ref('base.group_system')

# With raise_if_not_found
record = self.env.ref('module.xml_id', raise_if_not_found=False)
if record:
    # record exists
    pass

Environment Immutability

The environment is immutable. Methods like with_context(), with_company(), with_user(), and sudo() all return new recordsets with new environments. The original recordset is unchanged:

original = self.env['res.partner'].search([])
elevated = original.sudo()
french = original.with_context(lang='fr_FR')

# All three are different environments
original.env.su  # False
elevated.env.su  # True
french.env.context.get('lang')  # 'fr_FR'

Common Patterns

# Disable mail tracking for bulk operations
records.with_context(tracking_disable=True).write({
    'state': 'done'})

# Skip mail create notification
self.with_context(mail_create_nolog=True).create(vals)

# Force a specific language for a report
def _get_report_data(self):
    return self.with_context(
        lang=self.partner_id.lang or 'en_US'
    ).read(['name', 'description'])