Skip to content

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:

User groups assignment

  • 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:

  1. Log out.
  2. Log in as axis_user / axis_user.
  3. Try to open Inventory → Device Inventory → Delivery Manifests. You should see zero results — delivery manifests belong to Reyder.
  4. Open Settlement Reports. You should see only SETTLE/OWN/* rows, never SETTLE/CON/*.
  5. 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 default stock.rule_stock_lot_comp_rule is deactivated via active=False inherit.
  • settlement_report_rule[('company_id', 'in', company_ids)], default base rule
  • settlement_report_line_rule — inherits via report_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:

env['consignment.settlement.report'].with_user(5).search([])  # axis_user

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.