- 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
FAQ
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.
@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.
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
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.
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.
OWL Frontend Questions
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.
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
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.
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
Three layers: (1) Access rights — ir.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
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.

