Who can delete an invoice in your Odoo? Access rights, groups and record rules explained
TechnoVision
24 September 2026 · 8 min read
Ask an Odoo administrator "who can delete a posted invoice?" and the honest answer is usually "let me check". The user form shows checkboxes, but the checkboxes are not the permissions. The permissions are the result of four layers stacked on top of each other, and the layers combine in ways that surprise people.
This article walks through those layers using one concrete question, so that by the end you can answer it for your own database. It applies to Odoo 17, 18 and 19, Community and Enterprise; we point out where 19 changed the vocabulary.
The four layers, in one table
| Layer | Odoo model | Question it answers | Example |
|---|---|---|---|
| Groups | res.groups | Which roles does this user hold? | "Accounting / Accountant" |
| Access rights (ACL) | ir.model.access | Can this group read/write/create/delete this model? | Accountant can unlink account.move |
| Record rules | ir.rule | Which records of that model can they touch? | Only invoices of their own company |
| Field groups | groups= on a field | Can they see or edit this field? | Only managers see the cost price |
Odoo checks them in that order: no group, no ACL; ACL without a matching rule, no record; and field groups trim what is visible on top. Business logic (for example, "you cannot delete a posted invoice, reset it to draft first") is a fifth layer written in Python, outside the security model.
Layer 1: groups, implied groups and privileges
A user belongs to any number of groups. Two features make groups less obvious than they look:
- Implied groups. A group can imply others. Granting "Accounting / Administrator" silently adds "Accounting / Accountant", "Accounting / Billing" and often "Contact Creation". The user form does not list the implied ones; you only see the box you ticked.
- Categories vs privileges. In Odoo 17 and 18, groups are organised by category (the app), which is why the user form shows one dropdown per app. Odoo 19 reorganised this: groups are attached to privileges (
res.groups.privilege), which still group them by app but make the implication chain explicit and easier to trace. The behaviour is the same; the vocabulary changed.
The practical consequence: to know a user's real group set, you must resolve the implied chain, not read the form.
Layer 2: access rights (ir.model.access)
Each module ships a CSV with one line per model and group, with four booleans: perm_read, perm_write, perm_create, perm_unlink. Two rules matter:
- Access rights are additive. A user's rights on a model are the union of the rights from all their groups (including implied ones). There is no "deny" line. If any group grants unlink, the user has unlink.
- An ACL line with an empty group applies to everyone. Some models are deliberately open to all internal users this way.
So for account.move (invoices), the question becomes: which of the user's groups, direct or implied, has an ACL line with perm_unlink = 1? In standard Odoo the "Accounting / Accountant" group (or its equivalent in your version) carries it, and "Billing" does not; verify in your database since customisations change this.
Layer 3: record rules (ir.rule)
Once the ACL says "yes, this model", record rules decide which rows. A rule is a domain plus the same four permission flags. There are two kinds, and they combine differently:
- Global rules (no group set) intersect: if two global rules apply, both must be satisfied. The multi-company rule on invoices is global, which is why a user never sees another company's invoices no matter which groups they have.
- Group rules (attached to one or more groups) unify: if two group rules apply, either is enough.
- Then the global set and the group set intersect.
Odoo's own documentation states this precisely, and it is the single most misread part of Odoo security. A common mistake: adding a group rule "salespeople see only their own quotations" and forgetting that a second group rule on the same model for a group the user also holds ("sales managers see all") will unlock everything, because group rules OR together.
For deletion specifically, a rule with perm_unlink checked restricts which invoices can be deleted; a rule with it unchecked does not apply to deletion at all.
Layer 4: field groups
A field declared with groups="account.group_account_manager" disappears from views, from fields_get(), and from read/write for anyone outside that group. It does not affect deletion of the record, but it decides whether the person deleting could even see the amount.
Layer 5 (business logic): why "unlink allowed" still may not delete
Even with unlink rights on account.move and a matching rule, Odoo's accounting code refuses to delete a posted invoice: it must be cancelled or reset to draft first, and in a locked period even that is blocked. Those checks live in Python, not in the security tables. When an auditor asks "could user X delete invoice Y?", the truthful answer has to consider both the security model and the state of the record.
Answering the question step by step
Here is the manual procedure for "can user X delete invoices?", with developer mode on.
- List X's groups. Settings → Users → X → the Access Rights tab. Note every selection.
- Resolve implied groups. Settings → Users & Companies → Groups (Odoo 19: Privileges). Open each group and read "Inherits" / implied groups. Repeat until nothing new appears.
- Find ACLs for the model. Settings → Technical → Security → Access Rights. Filter model = Journal Entry (
account.move). Keep only lines whose group is in X's resolved set, or whose group is empty. If any line has Delete checked, X has unlink at the model level. - Find record rules for the model. Settings → Technical → Security → Record Rules. Filter model =
account.move. Separate global rules from group rules whose group is in X's set. Apply the AND/OR logic above with only rules that have Delete checked. - Check the record state. Posted? Locked period? Then business logic blocks the deletion regardless.
- Check for superuser. A user with the "Settings / Administrator" group who switches to superuser mode bypasses all of the above. This is the one you most want to find.
Doing this for one user and one model takes ten minutes. Doing it for forty users and every sensitive model (invoices, payments, payroll, contacts, purchase orders) is a day, and it is out of date the next time someone edits a group.
This is why our Claude connector runs every call as the token owner.
Common mistakes we see in Egyptian companies
- Everyone is an Accounting Administrator because that was the only way to make a report work in 2021. Nobody revisited it.
- The ex-employee's account is archived, not audited. Archiving stops login; it does not tell you what they could do while active, or whether an API key still exists.
- Custom modules with
group_idempty in their ACL CSV, silently granting everyone full rights on a new model. - Record rules written for the wrong company field, so multi-company isolation quietly stops applying to a custom model.
- Superuser mode left as the default way to "fix things", used from a shared account.
Hardening checklist
- Create a dedicated group "Accounting / Can delete invoices" with no implied groups, and move unlink on
account.moveto it. Assign it to one or two people. - Remove "Settings / Administrator" from everyone except a named admin and a break-glass account with a stored, rotated password.
- Review every ACL line with an empty group in custom modules.
- Add a global record rule on sensitive custom models for
company_id in company_ids. - Schedule a monthly review that answers, in writing, "who can delete invoices, payments and employees?".
Odoo 18 vs Odoo 19: what changed for admins
| Topic | Odoo 18 | Odoo 19 |
|---|---|---|
| Group organisation | Categories per app | Privileges (res.groups.privilege) per app |
| Implied groups | implied_ids on the group | Same, surfaced through the privilege |
| ACL and rule semantics | Additive ACL; global AND, group OR | Unchanged |
| User form | Dropdown per category | Dropdown per privilege, with clearer inheritance |
If you wrote documentation for your team on 17 or 18, the concepts survive into 19; the screens and names do not.
Frequently asked questions
What is the difference between access rights and record rules in Odoo?
Access rights decide whether a group can read, write, create or delete a whole model. Record rules decide which individual records those operations apply to, using a domain filter.
How do multiple record rules combine in Odoo?
Global rules (no group) are ANDed together. Group rules are ORed together. Then the global result is ANDed with the group result.
Why can a user see records I thought a record rule blocked?
Usually because they hold another group with a broader group rule on the same model; group rules OR together, so the broader one wins. Or the rule you wrote is not marked for the operation (read/write/delete) in question.
Can a user delete a posted invoice in Odoo?
Not directly. Even with unlink rights, Odoo's accounting logic requires the invoice to be cancelled or reset to draft first, and locked periods block it entirely. The security model and the record state both matter.
If you'd rather not build this yourself