A user with access to two companies picks a product while creating a sales order — and Odoo lets them pick one that belongs to the other company entirely, because nothing about the field they picked it from was told to check. Odoo’s own multi-company documentation names this exact scenario directly: a user logged into both company A and company B “could create a sales order in company A and add products belonging to company B” — and three separate, purpose-built mechanisms exist specifically to prevent it.
The short version: company_id marks which company owns a record. allowed_company_ids (in context) is the subset of a user's companies currently toggled on in the multi-company switcher — not their full access, just what's active right now. check_company=True on a relational field is what actually stops the field-picker cross-contamination bug above; without it, nothing enforces that a related record belongs to a compatible company.
Three mechanisms, three different jobs
| Mechanism | What it actually is | What it solves |
|---|---|---|
company_id | A plain many2one field on a record | Marks which company a record belongs to |
company_ids (on res.users) | Every company a user is allowed to access at all | The user’s full access scope |
allowed_company_ids (context) | The subset currently toggled on in the multi-company switcher | What’s active right now — record rules key off this, not the user’s full access |
check_company=True (on a field) | A validation attribute on a relational field | Prevents linking to a record from an incompatible company |
company_dependent=True (on a field) | A storage attribute on any field | Lets one record hold a genuinely different value per company |
Why allowed_company_ids, specifically, is what record rules check
It’s tempting to assume access rules check company_ids (the user’s full access) — they don’t. Security rules key off allowed_company_ids, the runtime subset of companies actively selected in the multi-company widget at that moment, precisely so that a user with access to five companies but only two currently toggled on sees exactly those two, not all five, without needing to log out and back in to narrow scope temporarily.
check_company: the field-level fix for the actual bug above
Per the same documentation, setting _check_company_auto = True on a model and marking its company-sensitive relational fields with check_company=True triggers an automatic validation on every create()/write(): “if a record has no company_id… it cannot be linked to a record whose company_id is set,” and two records with company_ids set both have to actually match. This is what closes the exact gap in the opening example — without it, a many2one from a sales order to a product has no built-in awareness that the two might belong to different companies at all; the relation is just a foreign key, agnostic to either side’s company_id unless something explicitly checks.
Watch out: check_company has to be set explicitly on each relevant field — it isn't inferred automatically just because a model has a company_id. A custom module adding a new many2one to a multi-company model needs to deliberately add check_company=True on any field where cross-company linking would actually be wrong; leaving it off doesn't error, it just leaves that one specific field exactly as exposed to the contamination bug as if the model had no multi-company awareness at all.
company_dependent: one record, a different value per company
A separate concern entirely: some fields need to hold a genuinely different value depending on which company is asking — a shared product’s cost price that differs by company, for instance, on the same underlying record rather than a separate record per company. company_dependent=True on a field’s definition handles exactly this: the value read and written automatically resolves against self.env.company, so the same Python code reading that field gets a different answer depending on which company the current environment is scoped to, with no explicit branching needed in the reading code.
Where this connects to record rules
The global, company_id-based record rule pattern mentioned in the access-rights article — something like ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] — is exactly what enforces “only see records belonging to a company you’re currently scoped to,” and it’s allowed_company_ids/the equivalent company_ids variable available in a record rule’s own domain that rule actually reads, not a user’s full, untoggled access list.
Frequently asked questions
Does allowed_company_ids ever include more companies than company_ids?
No — it can only ever be a subset of (or equal to) the user's actual access; the multi-company switcher only lets a user toggle among companies they already have access to, never one they don't.
Is check_company on by default for company_id-bearing models?
No — it requires explicitly setting _check_company_auto = True on the model and check_company=True on each relevant field; there's no implicit default that infers this just from a model having a company_id field.
Can a record with no company_id at all be linked to a record that does have one?
Per the documentation's own wording, a record with no company_id set is treated as company-agnostic and can be linked either way — the check specifically targets the case where *both* sides have a company_id set and they don't match.
Is company_dependent the same mechanism as a translated field?
Conceptually similar (one stored value resolving differently depending on context) but a separate mechanism — translated fields resolve by language, company-dependent fields resolve by the current company, and a field can be either, both, or neither depending on what it's declared with.
What's the simplest way to reproduce the cross-company contamination bug to test for it?
Log in as (or simulate) a user with both companies toggled on in the multi-company switcher, and attempt to link a record in company A to a related record explicitly created under company B — if nothing rejects it, the relevant field is missing check_company=True.
Further reading
- Multi-company Guidelines — official Odoo 18.0 developer documentation, the primary source for every mechanism above, including the exact cross-company contamination example this article opens with.
And on this site: Why Can’t This User See a Record They Should? for the record-rule side of multi-company visibility, and the Domain Filter Builder & Explainer for testing a company_id-based record rule’s domain directly.