Ad – 728×90
🎯 Interview Prep

Advanced Odoo Interview Questions – Senior Developer Q&A

Senior Odoo developer interviews probe deeper: you're expected to explain architecture decisions, performance trade-offs, and how Odoo's internals work. These questions cover OWL frontend architecture, POS internals, multi-company patterns, performance optimization, portal security, and deployment practices.

⏱️ 35 min 🎯 Advanced 📅 Updated 2026
Topics covered:
  • Performance and the ORM prefetch cache
  • Multi-company architecture
  • OWL and the frontend registry pattern
  • POS architecture and customization patterns
  • Portal security model
  • Deployment and migration strategy

Performance Questions

Explain the N+1 query problem in Odoo and how to avoid it.

N+1 occurs when you loop over a recordset and access a relational field one record at a time via browse() inside the loop — each access triggers a separate SQL query. Odoo's ORM prevents this through prefetch batching: when you iterate a recordset and access a field on any record, the ORM fetches that field for the entire recordset in a single query. The key rule: always browse() a complete list of IDs before looping — never browse inside a loop.

What is @tools.ormcache and what are its limitations?

@tools.ormcache caches the return value of a model method keyed on its arguments. The cache lives in memory per Odoo worker process and is invalidated when any record of the model is written or deleted. Limitations: (1) cache is not shared across workers — in multi-worker setups, each worker has its own cache; (2) cache is not persisted across restarts; (3) incorrect cache keys (forgetting self.env.uid when the result varies per user) can cause data leaks between users.

When would you use a stored computed field and what are the trade-offs?

Use store=True when: the field is searched/filtered in domain expressions, it's read on large lists (performance), or computing it on every read is expensive. Trade-offs: stored fields consume disk space, add write overhead (recomputed on every dependency change), and can cause cascading recomputes in complex dependency chains. Non-stored fields add read overhead but zero write overhead and no storage cost.

Multi-Company Questions

How does Odoo's multi-company isolation work and how do you implement it?

Multi-company isolation is achieved through company_id fields on records and record rules that filter by company_ids (the user's active companies). To implement it: (1) add a company_id = fields.Many2one('res.company', ...) field with a default of self.env.company; (2) add an ir.rule with domain ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]. Records with company_id=False are shared across all companies.

What is company_dependent and when would you use it?

company_dependent=True on a field means each company stores its own value without duplicating records. The ORM transparently reads/writes the value for self.env.company. Use it for configuration settings that differ per company (e.g., late fee rates, default email signatures, fiscal year settings) while keeping one configuration record per company rather than per record.

Ad – 728×90

OWL Frontend Questions

Explain the difference between using patch() and class inheritance for OWL components.

OWL class inheritance (class MyWidget extends CharField) creates a new class — use it when creating a new component type. patch() modifies an existing component's prototype in-place — use it when you want to extend a component that is already registered in Odoo's registries (like a POS component or a built-in field widget) without replacing its registry entry. If you used class inheritance for an existing widget, you'd also need to re-register it under the same key, which can break other modules inheriting the original.

How does the OWL registry pattern work and why is it used instead of direct imports?

The registry is a key→value store that components declare what they need without knowing who provides it. This allows multiple modules to register for the same key, override entries, or add new ones without modifying original files. For example, registry.category('fields').add('my_widget', ...) makes the widget available to any view that uses widget="my_widget" without the view needing to import it. This is essential for a plugin-based architecture where modules don't know about each other.

POS Architecture Questions

How does the Odoo POS handle offline operation?

At session open, the POS loads all required data (products, customers, taxes, payment methods) in a single batch from the server. Orders are created as JavaScript objects in memory. Completed orders are stored locally in IndexedDB if the server is unreachable. When connectivity is restored, they are synced via /pos/create_from_ui. This makes the POS resilient to temporary network loss during a session — core selling functionality works offline.

How do you add custom data to the POS session and make it available in JavaScript?

Override _get_pos_ui_data() on pos.config in Python and add your model's data under a key in the returned dict: data['my.model'] = self.env['my.model'].search_read([...], fields=[...]). On the JavaScript side, the data is available after session load. Register the model in the POS DB to index it for fast lookups: this.env.pos.db.add_records('my.model', data).

Portal Security Questions

What are the security layers in an Odoo portal integration and what is the most common mistake?

Three layers: (1) Access rightsir.model.access.csv gives base.group_portal read access; (2) Record rules — an ir.rule restricts portal users to their own records; (3) _document_check_access — validates ownership or access token in the controller before rendering a detail page. The most common mistake is forgetting the record rule — without it, a portal user with read access can call search([]) and enumerate all records.

Deployment Questions

What is the purpose of a migration script and when does it run?

Migration scripts handle data transformations when the module's version number changes. They run during odoo-bin -u in version order: pre_migrate.py runs before new field definitions are applied (use for column renames before ORM recreates them), and post_migrate.py runs after (use for data backfills). They receive the raw database cursor — not the ORM — because the ORM state may not match the database schema during migration.