If you’ve worked on even a single custom Odoo module, you’ve typed self.env or called sudo() without fully understanding what’s happening under the hood. These two are the backbone of the Odoo ORM. Get them right, and your module code becomes clean, secure, and efficient. Get them wrong, and you’ll end up with permission errors, security holes, or puzzling bugs that take hours to trace.
In this guide, I’ll break down how to use env and sudo in Odoo ORM from the perspective of someone who builds and maintains production Odoo modules. We’ll cover what these objects are, how they work together, and when to use sudo(), when NOT to use them, and real-world code examples you can use in your own modules today.
Table of Contents
What is env in Odoo ORM?
In Odoo, env is short for environment. It is an instance of the odoo.api.Environment class, and it acts as the central gateway to everything in Odoo’s ORM layer. Think of it as a context object that knows three critical things:
- Which database cursor (
cr) is active - Which user (
uid) is executing the current code - What contextual data (
context) is being passed around
Every time you write self.env['res.partner'] You’re asking the environment to give you access to the res.partner model on behalf of the current user, using the current transaction’s database cursor.
This matters more than it sounds. The environment is not just a convenience wrapper; it is the object that enforces access control. When you access a model through env Odoo checks the current user’s permissions before returning or writing data.
# Accessing a model through env
partners = self.env['res.partner'].search([('customer_rank', '>', 0)])PythonThe above line returns all partner records where customer_rank > 0, but only if the current user has read access to res.partner. If they don’t, Odoo raises an AccessError. That is env doing its job.
Key Attributes of the Odoo Environment
The env object exposes several important attributes that you’ll use constantly in Odoo development.
env.cr: The Database Cursor
self.env.cr.execute("SELECT id FROM res_partner WHERE customer_rank > 0")
rows = self.env.cr.fetchall()Pythonenv.cr is the raw PostgreSQL cursor for the current transaction. Use it only when you need raw SQL that the ORM cannot handle efficiently. Always prefer ORM methods over raw SQL for security and compatibility.
env.uid: Current User ID
current_user_id = self.env.uid # Returns an integer, e.g., 7PythonThis gives you the numeric ID of the user executing the current operation. It’s useful for logging, conditional logic, or filtering records by the current user.
env.user: Current User Record
user = self.env.user
print(user.name) # e.g., "John Doe"
print(user.email) # e.g., "john@example.com"
print(user.company_id) # The user's companyPythonenv.user is a full res.users recordset, giving you access to all fields on the user record. This is more useful than env.uid in most situations.
env.context: The Context Dictionary
context = self.env.context
lang = context.get('lang', 'en_US')
active_id = context.get('active_id')PythonThe context is a read-only dictionary that carries metadata through the call stack, the active language, active record IDs, flags to skip certain logic, and more. You can create a modified environment with a new context using with_context().
# Create a new env with modified context
env_in_french = self.env.with_context(lang='fr_FR')
product = env_in_french['product.template'].browse(1)
# product.name now returns the French translationPythonenv.company: Current Company
company = self.env.company
print(company.name) # e.g., "Bit Level Code"
print(company.currency_id.name) # e.g., "USD"PythonIn multi-company setups, this is the active company for the current session. Critical for any accounting, pricing, or company-specific logic.
How to Access env in Different Contexts
Inside a Model Method
This is the most common case. When you’re inside a method of any model that inherits from models.Model, self.env is always available.
from odoo import models, fields
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm_custom(self):
# self.env is always available inside model methods
config = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
return TrueHow to Use env and sudo in Odoo ORMInside a Wizard (TransientModel)
Wizards also inherit from models.Model via models.TransientModel, so self.env works identically.
class StockReturnPicking(models.TransientModel):
_name = 'stock.return.picking'
def create_returns(self):
move_obj = self.env['stock.move'] # Access models through env
# ...PythonInside the Odoo Shell
When using the Odoo Shell for debugging or scripting, env is pre-loaded for you.
# In Odoo Shell
partners = env['res.partner'].search([('is_company', '=', True)])
print(partners.mapped('name'))PythonWhat is sudo in Odoo ORM?
sudo(): It is a method available on every Odoo recordset. It returns a new version of the same recordset (or a new model accessor), but with elevated privileges; specifically, it bypasses Odoo’s access control rules (ACLs) and record rules.
In simple terms, when you call sudo, the subsequent ORM operations run as if the administrator user is performing them, regardless of who is actually logged in.
# Normal: raises AccessError if current user has no access to ir.config_parameter
config = self.env['ir.config_parameter'].get_param('web.base.url')
# With sudo(): always works, bypasses ACL check
config = self.env['ir.config_parameter'].sudo().get_param('web.base.url')PythonHow sudo() Works Internally
Calling sudo() on a recordset, creates a new Environment object where uid is replaced with the administrator user’s ID (by default, uid=1: the OdooBot/admin user). All ORM operations performed through that new environment skip the standard access control checks.
It does not:
- Bypass Python-level
@api.constrainsvalidations - Bypass SQL-level constraints
- Grant access to methods decorated with explicit security checks
- Allow cross-company data access if multi-company rules are enforced at the SQL level
It does:
- Bypass
ir.model.access(ACL) rules - Bypass
ir.rulerecord rules (domain-based row-level security) - Allow reading/writing fields that would otherwise be restricted
When to Use sudo in Odoo
1. Reading System Configuration Parameters
ir.config_parameter is typically restricted to the admin user. If a regular user triggers your code (e.g., confirming a sale order), you’ll need sudo() to read system settings.
def _get_base_url(self):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
return base_urlPython2. Logging to Models the User Can’t Write To
def action_process(self):
# Log an activity even if user doesn't have write access to mail.activity
self.env['mail.activity'].sudo().create({
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
'res_model_id': self.env['ir.model']._get('sale.order').id,
'res_id': self.id,
'user_id': self.env.uid,
'summary': 'Order processed by automation',
})Python3. Cross-Model Operations in Automated Actions
When your model method touches another model that the triggering user may not have access to:
def _auto_assign_salesperson(self):
# The portal user who triggered this may not have hr.employee access
employees = self.env['hr.employee'].sudo().search([
('department_id.name', '=', 'Sales'),
('active', '=', True)
], limit=1)
if employees:
self.user_id = employees[0].user_idPython4. Sending Emails Programmatically
def send_notification_email(self):
template = self.env.ref('your_module.email_template_order_done')
template.sudo().send_mail(self.id, force_send=True)Python5. Creating Records in Restricted Models from a Portal User Action
Portal users have very limited access. If a portal user submits a form that should create a backend record:
@api.model
def create_from_portal(self, vals):
# Portal user cannot write to sale.order directly
order = self.env['sale.order'].sudo().create(vals)
return order.idHow to Use env and sudo in Odoo ORMWhen NOT to Use sudo() in Odoo
This is the part most Odoo tutorials skip, and it’s critically important.
Don’t use sudo() to bypass security restrictions
If a user doesn’t have access to delete records, that restriction exists for a reason. Using sudo() to delete records on behalf of that user is a security vulnerability.
# BAD PRACTICE — bypasses intentional security
def user_requested_delete(self):
self.sudo().unlink() # DON'T DO THIS for user-initiated actionsPythonDon’t use sudo() in methods exposed to untrusted input
If a method is callable from a website/portal controller and takes user-supplied data, using sudo() inside it opens the door to privilege escalation.
# DANGEROUS — user input going through sudo()
@http.route('/web/create', type='json', auth='public')
def create_record(self, model_name, vals):
# Never do this!
record = request.env[model_name].sudo().create(vals)PythonDon’t use sudo() as a lazy shortcut
The most common mistake junior Odoo developers make is adding sudo() everywhere to stop AccessError exceptions without understanding why the error occurred. This approach leads to modules with no effective security model.
# LAZY and WRONG — find out why the user doesn't have access instead
def action_something(self):
self.env['some.model'].sudo().search([...]) # Why can't they access it?PythonUsing sudo() with a Specific User
Starting from Odoo 12, sudo() accepts a user argument. Instead of running as admin, you can run as any specific user:
# Run as a specific user by passing the user record
specific_user = self.env['res.users'].browse(5)
records = self.env['sale.order'].sudo(specific_user).search([])
# Or pass the user ID directly
records = self.env['sale.order'].sudo(5).search([])PythonThis is extremely useful for testing, scheduled actions, or any scenario where you want the ORM to behave as a particular user (with their specific company context, language, and access level) without physically being logged in as them.
env.user vs sudo() – What’s the Difference?
This confuses a lot of developers. Here’s the clear distinction:
| env.user in Odoo | sudo() in Odoo | |
| What it is | The current logged-in user record | A method to bypass access checks |
| Changes who is running? | No | Yes (to admin, or specified user) |
| Affects ACL checks? | No | Yes: bypasses them |
| Use for reading user info? | Yes | No |
| Use for elevated operations? | No | Yes |
# env.user — READ info about who is running the code
name = self.env.user.name # "Mukesh Patel"
lang = self.env.user.lang # "en_IN"
groups = self.env.user.groups_id # Groups the user belongs to
# sudo() — EXECUTE with elevated permissions
config = self.env['ir.config_parameter'].sudo().get_param('some.key')PythonPractical Code Examples
Example 1: Auto-populate a field using system config
from odoo import models, fields, api
class ProjectTask(models.Model):
_inherit = 'project.task'
base_url = fields.Char(compute='_compute_base_url')
def _compute_base_url(self):
url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
for record in self:
record.base_url = urlPythonExample 2: Create a log entry regardless of user permissions
def action_approve(self):
self.state = 'approved'
# Log in audit model — current user may not have create rights
self.env['my.module.audit.log'].sudo().create({
'model': self._name,
'record_id': self.id,
'action': 'approved',
'user_id': self.env.uid, # Still log the actual user!
'timestamp': fields.Datetime.now(),
})PythonExample 3: Using with_user() for multi-user logic
def check_user_access(self, user_id):
target_user = self.env['res.users'].browse(user_id)
# Check what records THIS specific user can see
visible_orders = self.env['sale.order'].with_user(target_user).search([])
return len(visible_orders)PythonExample 4: Safe sudo() pattern – always preserve the original user context
def process_background_task(self):
original_uid = self.env.uid # Preserve who triggered this
# Do elevated work
orders = self.env['sale.order'].sudo().search([('state', '=', 'draft')])
for order in orders:
# Log with the real user, not sudo
order.sudo().message_post(
body=f"Auto-processed by user ID {original_uid}",
author_id=self.env['res.partner'].sudo().browse(
self.env['res.users'].sudo().browse(original_uid).partner_id.id
).id
)How to Use env and sudo in Odoo ORMCommon Mistakes and How to Avoid Them
Mistake 1: Losing the user context after sudo()
When you use sudo(), env.user inside that sudo context becomes the admin user. If you need to reference the original user, capture self.env.uid before calling sudo().
Mistake 2: Forgetting that sudo() on a recordset propagates
order = self.env['sale.order'].sudo().browse(1)
# order.partner_id is now also accessed with sudo() - sudo() propagates through relationsPythonThis is usually fine, but be aware that related record access also gets elevated.
Mistake 3: Using sudo() in @api.constrains
Constraints should enforce business rules. Using sudo() them inside to bypass checks creates contradictory logic. Avoid it unless there’s a very specific reason.
Mistake 4: sudo() in website controllers with no input validation
Always validate and sanitize any user input before it touches sudo() code. Never pass raw request parameters directly into sudo() ORM calls.
Related Posts:
>> How to Override a View in Odoo 17 (XPath guide)
>> What is Odoo Runbot and How to Access It?
>> How to Configure Odoo with Nginx as a Reverse Proxy
>> How to Create Gantt View in Odoo 17
>> How to Add a Custom Filter in Odoo
FAQ for how to use env and sudo in Odoo ORM
Q1. What does env stand for in Odoo?
env stands for environment. It is an instance of odoo.api.Environment and provides access to the ORM, the current user, the database cursor, and the context. It is the primary interface between your Python code and the Odoo database.
Q2. Does sudo() make code run as the admin user?
By default, yes. Calling sudo() without arguments, runs subsequent ORM operations as the admin user (UID 1), bypassing all access control rules. You can also pass a specific user: sudo(user_record) or sudo(user_id) to run as any user.
Q3. Is it safe to use sudo() in production Odoo modules?
It depends on context. Using sudo() to read system parameters or create log entries is safe and common. Using it to bypass user-level security checks on user-initiated actions is a security risk. Always ask: “Should this user have the ability to trigger this elevated operation? Am I validating their input before using sudo()?”
Q4. What is the difference between sudo() and with_user() in Odoo?
sudo() bypasses access control checks entirely (runs as admin by default). with_user(user) changes the current user in the environment but does NOT bypass access checks; the user’s normal permissions still apply. Use with_user() when you want to execute code in the context of a specific user while still respecting their permissions.
Q5. Can sudo() bypass @api.constrains validations?
No. sudo() bypasses ir.model.access (ACL) and ir.rule (record rules), But Python-level constraints defined with @api.constrains are always executed regardless of sudo status. Database-level SQL constraints are also always enforced.
Q6. How do I use env in the Odoo Shell?
In the Odoo Shell, env is pre-loaded for you. You can use it directly: env[‘res.partner’].search([]). By default, it runs as the admin user. To run as a different user: env(user=env[‘res.users’].browse(5))[‘res.partner’].search([]).
Q7. What happens if I chain sudo multiple times?
Chaining sudo() multiple times has no additional effect. record.sudo().sudo() is the same as record.sudo(). The environment is already elevated after the first call.
Final Words: How to Use env and sudo in Odoo ORM
Understanding env and sudo in Odoo ORM is not optional knowledge for an Odoo developer it is foundational. Every module you write will use self.env to access models, and most production modules will use sudo() in at least one or two places for legitimate system-level operations.
The key takeaways are:
envis your gateway to the ORM: it carries the user context, database cursor, and contextual dataenv.user,env.uid,env.cr,env.context, andenv.companyare your most-used attributessudo()bypasses access control: use it deliberately, not as a lazy fix- Always preserve
self.env.uidwhen logging actions inside sudo() blocks - Prefer
with_user()oversudo(): When you want user context without elevated access - Never use
sudo()on user-supplied data without thorough validation
I hope this article, “How to Use env and sudo in Odoo ORM” will help you. Master these two concepts, and your Odoo module code will be cleaner, safer, and easier to debug.



