refactor: replace global record rule with ORM overrides on account.journal to enforce journal access restrictions safely

This commit is contained in:
Suherdy Yacob 2026-05-29 20:47:57 +07:00
parent 983e966f1a
commit 4505c7c486
5 changed files with 83 additions and 15 deletions

View File

@ -8,14 +8,17 @@ This module allows restricting users to specific journals in Odoo's Accounting a
- **Access Control:**
- If "Allowed Journals" is **empty**, the user has access to **ALL** journals (standard behavior).
- If "Allowed Journals" is **populated**, the user can **ONLY** access the selected journals.
- Implements a global Record Rule to enforce this restriction across the system (Views, Search, Create, Write).
- Implements an ORM-level `_search`, `write`, and `unlink` override on `account.journal` to safely and recursively filter journals without crashing on standard views.
- **Context-Aware Bypass:** Automatically detects and bypasses restrictions during critical Point of Sale (POS) operations and administrative automated/clearing background contexts.
## Configuration
1. Go to **Settings** > **Users & Companies** > **Users**.
2. Open the user you want to restrict.
3. Go to the **Allowed Journals** tab.
4. Add the journals the user is allowed to access.
5. Save.
3. In the **Allowed Journals** field, select the journals the user is allowed to access.
4. Save.
## Author
Suherdy Yacob
## License
LGPL-3

View File

@ -9,10 +9,9 @@
- If "Allowed Journals" is set, the user can only access those journals.
- If "Allowed Journals" is empty, the user can access all journals.
""",
'author': 'Antigravity',
'author': 'Suherdy Yacob',
'depends': ['base', 'account'],
'data': [
'security/account_security.xml',
'views/res_users_views.xml',
],
'installable': True,

View File

@ -1 +1,2 @@
from . import res_users
from . import account_journal

74
models/account_journal.py Normal file
View File

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class AccountJournal(models.Model):
_inherit = 'account.journal'
@api.model
def _search(self, domain, offset=0, limit=None, order=None, **kwargs):
"""
Override _search to filter journals based on the allowed_journal_ids configuration on res.users.
This provides a secure, recursive-safe, and context-aware filtering mechanism.
"""
user = self.env.user
# Determine if we should bypass the journal visibility restrictions:
# 1. Superuser / system context (env.su) is always bypassed.
# 2. Skip if user has no allowed journals configured (empty means access to all).
# 3. Explicit bypass requested in the context (bypass_allowed_journal).
# 4. Point of Sale contexts:
# - pos_session_id or pos_config_id is present.
# - pos_last_server_date is present (POS frontend loading data).
# 5. During standard module installations/upgrades (install_mode).
if not self.env.su and user.sudo().allowed_journal_ids:
bypass = (
self.env.context.get('bypass_allowed_journal') or
self.env.context.get('pos_session_id') or
self.env.context.get('pos_config_id') or
'pos_last_server_date' in self.env.context or
self.env.context.get('install_mode')
)
if not bypass:
allowed_ids = user.sudo().allowed_journal_ids.ids
domain = [('id', 'in', allowed_ids)] + list(domain)
return super(AccountJournal, self)._search(domain, offset=offset, limit=limit, order=order, **kwargs)
def write(self, vals):
"""
Restrict write access to allowed journals only.
"""
if not self.env.su:
user = self.env.user
if user.sudo().allowed_journal_ids:
bypass = (
self.env.context.get('bypass_allowed_journal') or
self.env.context.get('pos_session_id') or
self.env.context.get('pos_config_id')
)
if not bypass:
allowed_ids = user.sudo().allowed_journal_ids.ids
unallowed = self.filtered(lambda j: j.id not in allowed_ids)
if unallowed:
raise UserError(_("You are not allowed to modify the following journal(s): %s") % ", ".join(unallowed.mapped('name')))
return super(AccountJournal, self).write(vals)
def unlink(self):
"""
Restrict delete access to allowed journals only.
"""
if not self.env.su:
user = self.env.user
if user.sudo().allowed_journal_ids:
bypass = (
self.env.context.get('bypass_allowed_journal') or
self.env.context.get('pos_session_id') or
self.env.context.get('pos_config_id')
)
if not bypass:
allowed_ids = user.sudo().allowed_journal_ids.ids
unallowed = self.filtered(lambda j: j.id not in allowed_ids)
if unallowed:
raise UserError(_("You are not allowed to delete the following journal(s): %s") % ", ".join(unallowed.mapped('name')))
return super(AccountJournal, self).unlink()

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="journal_comp_rule_allowed_journals" model="ir.rule">
<field name="name">Journal Multi-Company with Allowed Journals</field>
<field name="model_id" ref="account.model_account_journal"/>
<field name="domain_force">[(1, '=', 1)] if not user.allowed_journal_ids else [('id', 'in', user.allowed_journal_ids.ids)]</field>
<field name="global" eval="True"/>
</record>
</odoo>