Security & Multi-Company¶
Who this is for
The operator setting up users, and anyone verifying that the data-separation story actually holds up (e.g. before an audit). Day-to-day users usually don't need this page.
What you'll accomplish: understand who can see what, verify the separation works, and fix access-control problems when they surface.
The business rule in plain English¶
Two companies share one Odoo database:
- Reyder Enterprises — runs the operation, owns customer relationships, handles every SO and every invoice. Sees everything.
- Axis Mobile — owns the devices, receives settlement payments. Should see only their own devices and their own owner-settlement reports. Must not see Reyder's customer names, sale prices, SO numbers, or internal manifests.
This isn't a convention or a "please don't look" — it's enforced by Odoo record rules that run on every database query. Even a privileged Axis user can't pull Reyder's customer data through the ORM, the API, or the UI.
At-a-glance comparison¶
| An Axis user sees | A Reyder user sees |
|---|---|
| Their devices (IMEI, model, grade, status) | Their own + Axis's devices (via consignment) |
| Owner settlement reports only | Both owner and consignee reports (consignee for their own company) |
| Their consignment agreements | Their consignment agreements |
Reserved state on their devices (via stock.lot) — not the reservation record itself |
Full retail reservations (since Reyder is the holding company) |
| Device traceability (where Axis's devices are) | Full warehouse operations |
| — no customer names | All customer data |
| — no sale prices | All pricing |
| — no SO numbers | All SOs |
| — no Reyder manifests | All manifests |
Set up users the right way¶
Assign people to the correct company¶
A user who works for Axis should belong only to Axis. A user who works for Reyder should belong only to Reyder. Only system administrators should belong to both.
Open Settings → Users & Companies → Users and edit the user:

- Allowed Companies — tick only the companies this user works for
- Default Company — what they log into
If someone legitimately works for both, they belong to both — and the system will auto-expand their company context (see the gotcha below). But defaulting everyone to both-companies breaks the separation test.
Test users¶
| Login | Belongs to | Purpose |
|---|---|---|
admin |
Both companies | Operator, full access |
axis_user |
Axis Mobile only | For testing that Axis can't see Reyder data |
Always use axis_user to verify isolation
admin belongs to both companies, so Odoo's company_ids context includes both and every record rule evaluates to True. You cannot test separation as admin. Log in as axis_user instead — when they can't see something, that's the isolation working.
What enforces the separation¶
The protection is layered. Each layer stands alone — you'd need to defeat all three to leak data.
Layer 1 — Record rules (the primary enforcement)¶
Odoo record rules filter every read/write at the database level. The module adds these rules on top of the stock defaults:
| Model | Domain | Effect |
|---|---|---|
| Settlement report | company_id in company_ids |
Axis sees only their owner reports; Reyder sees only their consignee reports |
| Settlement report line | Follows parent report | Lines are only visible if you can see the report |
| Device manifest | company_id in company_ids |
Each company sees only their manifests |
| Consignment agreement | owner in company_ids OR consignee in company_ids |
Both parties to an agreement can see it |
stock.lot (device) |
Custom — see below | Consignee can see owner's devices when an agreement is active |
| Transfer order | source OR destination in company_ids |
Both parties see it |
| Retail sale | company_id in company_ids |
Company-scoped |
| Retail reservation | holding_company_id in company_ids |
Holding company only — owners do not see the reservation record directly; they see reserved state on stock.lot instead |
| SO line device allocation | sale_order_id.company_id in company_ids |
Follows parent SO |
Layer 2 — Data not stored at all on owner records¶
Even if Layer 1 failed, there'd still be nothing incriminating to see. When the system creates an owner settlement report, it deliberately does not populate sale_price, sale_order_name, or customer_name on those report lines. Those fields are blank at the row level.
If you dump the consignment_settlement_report_line table directly, the rows belonging to owner reports have nothing sensitive in them.
Layer 3 — No cross-reference from owner to consignee¶
The paired_report_id field only points consignee → owner. There's no reverse reference from the owner report to the consignee report. Even ORM-level access via browse() wouldn't get you there.
Layer 4 — View-level hiding¶
On the settlement report form, sale_order_id is wrapped in invisible="report_type == 'owner'" — so the field isn't rendered in the owner-side UI. This is defensive; it matters only if Layers 1–3 somehow all failed.
The device visibility rule (stock.lot)¶
The module replaces Odoo's default stock.lot record rule with a custom one that handles consignment:
A user can see a device if any of these is true:
1. company_id is in their company list (the device belongs to them), OR
2. company_id is False (unassigned), OR
3. owner_company_id is in their company list (they own it — Axis sees Axis's devices), OR
4. consignee_company_ids contains one of their companies (they're an authorized consignee — Reyder sees Axis's devices that have an active Reyder-Axis agreement)
Rule 4 is the consignment magic. It's what makes Axis's inventory visible to Reyder the moment an agreement is activated, and invisible again the moment it's terminated.
See Consignment Agreements — Under the hood for how consignee_company_ids is computed.
Verifying the separation (manual test)¶
Want to confirm it works? Run this quick test:
- Log out.
- Log in as
axis_user/axis_user. - Try to open Inventory → Device Inventory → Delivery Manifests. You should see zero results — delivery manifests belong to Reyder.
- Open Settlement Reports. You should see only
SETTLE/OWN/*rows, neverSETTLE/CON/*. - Click into an owner report. There's no Customer field, no Sale Price, no Sale Order number.
If any of those tests show Reyder data, something's broken — most likely axis_user accidentally got added to Reyder's company too, or a record rule got disabled.
Common problems¶
Axis user is seeing Reyder data
Check Settings → Users and open the Axis user. Confirm Allowed Companies lists only Axis Mobile. If Reyder is in there too, remove it. This is the #1 cause of leaks.
'admin' doesn't see separation when I test as admin
By design — admin is in both companies so Odoo's multi-company context includes both. Use axis_user for isolation testing, not admin.
Reyder user can't see Axis devices
The consignment agreement isn't Active, or it was just activated and the computed consignee_company_ids field hasn't been recomputed on existing devices. Open the agreement, click Suspend then Activate again — that forces _recompute_device_consignees() to run across all Axis's devices.
A settlement report is visible to both companies
That shouldn't happen. Check the report's company_id. For owner reports it should be Axis; for consignee reports it should be Reyder. If the field got written wrong, the record rule won't filter correctly. Fix the company_id directly in the DB or delete and let the system regenerate.
Cron jobs run as SUPERUSER and might post data that bypasses rules
Correct — SUPERUSER_ID=1 bypasses all record rules. This is intentional for crons and backend processes. The protection is at read time for user sessions. Cron-generated data is written by the system on both sides; it doesn't accidentally leak during a read because the record rule still filters what users see.
Under the hood¶
Technical details for developers
Access control (ir.model.access.csv) — 57 entries. General pattern:
| Group | Access |
|---|---|
stock.group_stock_user |
Read on most models |
stock.group_stock_manager |
Full CRUD |
sales_team.group_sale_salesman |
Create/edit sale.order.line.device |
sales_team.group_sale_manager |
Full sales + settlements |
Custom record rules — defined in security/security.xml. Key ones:
stock_lot_consignment_rule— replaces default with the 4-condition rule above. The defaultstock.rule_stock_lot_comp_ruleis deactivated viaactive=Falseinherit.settlement_report_rule—[('company_id', 'in', company_ids)], default base rulesettlement_report_line_rule— inherits viareport_id.company_id
Company-scoped writes using sudo()
Creating an owner settlement report happens from a Reyder user context, but the report belongs to Axis. Code uses .sudo() to cross the boundary:
self.env['consignment.settlement.report'].sudo().create({
'company_id': axis_mobile.id,
'report_type': 'owner',
...
})
See device_manifest.py:_process_customer_delivery() for the full pattern.
Testing isolation in the shell
odoo shell runs as SUPERUSER_ID=1 which bypasses all record rules. To simulate a specific user's view:
The with_user() call re-evaluates record rules as that user would see them.
The cids cookie
Odoo's company switcher reads cids from the user's session. If a user belongs to both companies, Odoo expands cids to include both whenever they navigate to a record in either company. This means a dual-company user sees everything they have access to, regardless of what's in their "currently selected" company. Single-company users (like axis_user) cannot be auto-expanded because the other company isn't in their allowed list.
Odoo 19 rename
res.users.groups_id was renamed to res.users.group_ids in Odoo 19. All internal code in this module uses group_ids.