From 8c13f76c938edde3a56b0ce163a371c94394e548 Mon Sep 17 00:00:00 2001 From: "Ignacio J. Ortega" Date: Thu, 9 Apr 2026 08:03:26 +0200 Subject: [PATCH 1/2] [FIX] account_operating_unit: filter payment register journals by OU When registering a payment from the invoice form, the payment wizard shows journals from all operating units instead of filtering by the invoice's operating unit. This causes AccessError when users with restricted OU access try to create payments. Override _get_batch_available_journals in account.payment.register to filter journals by the invoice's operating unit when all invoices in the batch belong to the same OU. --- account_operating_unit/__init__.py | 1 + .../tests/test_account_operating_unit.py | 41 +++++++++++++++++++ account_operating_unit/wizard/__init__.py | 1 + .../wizard/account_payment_register.py | 21 ++++++++++ 4 files changed, 64 insertions(+) create mode 100644 account_operating_unit/wizard/__init__.py create mode 100644 account_operating_unit/wizard/account_payment_register.py diff --git a/account_operating_unit/__init__.py b/account_operating_unit/__init__.py index 1fc45468f5..3244c50a00 100644 --- a/account_operating_unit/__init__.py +++ b/account_operating_unit/__init__.py @@ -2,3 +2,4 @@ from . import models from . import report +from . import wizard diff --git a/account_operating_unit/tests/test_account_operating_unit.py b/account_operating_unit/tests/test_account_operating_unit.py index 2f46fcef34..506107c79c 100644 --- a/account_operating_unit/tests/test_account_operating_unit.py +++ b/account_operating_unit/tests/test_account_operating_unit.py @@ -2,6 +2,7 @@ # © 2019 Serpent Consulting Services Pvt. Ltd. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +from odoo import fields from odoo.models import Command from odoo.tests import tagged @@ -181,3 +182,43 @@ def _prepare_invoice(self, operating_unit_id, name="Test Supplier Invoice"): "invoice_line_ids": lines, } return inv_vals + + def test_payment_register_journals_filtered_by_ou(self): + """Payment register wizard should only show journals matching the + invoice's operating unit. A user with access to a single OU must + not see journals from other OUs, and creating a payment must not + raise AccessError.""" + inv_vals = self._prepare_invoice(self.b2b.id, name="Test B2B Invoice") + inv_vals["invoice_date"] = fields.Date.today() + invoice = ( + self.move_model.with_user(self.user1) + .with_context(default_move_type="in_invoice") + .create(inv_vals) + ) + invoice.action_post() + + ctx = { + "active_model": "account.move", + "active_ids": invoice.ids, + } + wizard = ( + self.register_payments_model.with_user(self.user1) + .with_context(**ctx) + .create({"journal_id": self.cash2_journal_b2b.id}) + ) + + available_journals = wizard.available_journal_ids + for journal in available_journals: + self.assertTrue( + not journal.operating_unit_id or journal.operating_unit_id == self.b2b, + f"Journal '{journal.name}' (OU={journal.operating_unit_id.name})" + f" should not be available for OU {self.b2b.name}", + ) + + self.assertNotIn( + self.cash_journal_ou1, + available_journals, + "OU1 journal should not be available when paying a B2B invoice", + ) + + wizard.action_create_payments() diff --git a/account_operating_unit/wizard/__init__.py b/account_operating_unit/wizard/__init__.py new file mode 100644 index 0000000000..019698a882 --- /dev/null +++ b/account_operating_unit/wizard/__init__.py @@ -0,0 +1 @@ +from . import account_payment_register diff --git a/account_operating_unit/wizard/account_payment_register.py b/account_operating_unit/wizard/account_payment_register.py new file mode 100644 index 0000000000..868feb8649 --- /dev/null +++ b/account_operating_unit/wizard/account_payment_register.py @@ -0,0 +1,21 @@ +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import api, models + + +class AccountPaymentRegister(models.TransientModel): + _inherit = "account.payment.register" + + @api.model + def _get_batch_available_journals(self, batch_result): + """Filter available journals by the operating unit of the invoices.""" + journals = super()._get_batch_available_journals(batch_result) + lines = batch_result.get("lines") + if lines: + invoice_ous = lines.move_id.operating_unit_id + if invoice_ous and len(invoice_ous) == 1: + journals = journals.filtered( + lambda j: not j.operating_unit_id + or j.operating_unit_id == invoice_ous + ) + return journals From 35f9779486b41018b3b90afddf1ae49051d6747e Mon Sep 17 00:00:00 2001 From: "Ignacio J. Ortega" Date: Mon, 13 Apr 2026 18:16:46 +0200 Subject: [PATCH 2/2] [ADD] account_operating_unit: TDD test for payment register OU filter The 8c13f76c fix added _get_batch_available_journals override but shipped without a regression test. This adds one: a B2B-only user pays a B2B invoice via the Register Payment wizard, and the available_journal_ids must NOT include OU1 journals. --- account_operating_unit/tests/__init__.py | 1 + .../tests/test_payment_register_journal_ou.py | 151 ++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 account_operating_unit/tests/test_payment_register_journal_ou.py diff --git a/account_operating_unit/tests/__init__.py b/account_operating_unit/tests/__init__.py index 68197dab28..4ee788f44a 100644 --- a/account_operating_unit/tests/__init__.py +++ b/account_operating_unit/tests/__init__.py @@ -5,4 +5,5 @@ from . import test_cross_ou_journal_entry from . import test_operating_unit_security from . import test_payment_operating_unit +from . import test_payment_register_journal_ou from . import test_account_reconcile diff --git a/account_operating_unit/tests/test_payment_register_journal_ou.py b/account_operating_unit/tests/test_payment_register_journal_ou.py new file mode 100644 index 0000000000..c47f792f8a --- /dev/null +++ b/account_operating_unit/tests/test_payment_register_journal_ou.py @@ -0,0 +1,151 @@ +# © 2026 BITVAX +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +"""Bug: account.payment.register wizard's available_journal_ids +includes journals from operating units the invoice does not belong to. + +A B2B invoice should only offer journals whose operating_unit_id +matches B2B (or no OU). On upstream/18.0 the wizard pulls journals via +``account.journal.search()`` without OU context, so it returns every +sale/cash journal of the company regardless of OU. + +The fix is to override +``account.payment.register._get_batch_available_journals`` and filter +the returned set by the operating_unit_id of the invoices in the +batch. +""" + +from odoo.models import Command +from odoo.tests import tagged + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.addons.operating_unit.tests.common import OperatingUnitCommon + + +@tagged("post_install", "-at_install") +class TestPaymentRegisterJournalOu(AccountTestInvoicingCommon, OperatingUnitCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + (cls.ou1 | cls.b2b | cls.b2c).sudo().write({"company_id": cls.company.id}) + + cls.env.user.sudo().write( + { + "groups_id": [ + Command.link( + cls.env.ref("operating_unit.group_manager_operating_unit").id + ), + ], + "operating_unit_ids": [ + Command.link(cls.ou1.id), + Command.link(cls.b2b.id), + ], + "default_operating_unit_id": cls.ou1.id, + "company_ids": [Command.link(cls.company.id)], + "company_id": cls.company.id, + } + ) + + cls.user1.write( + { + "groups_id": [ + ( + 3, + cls.env.ref("operating_unit.group_manager_operating_unit").id, + ), + Command.link( + cls.env.ref("operating_unit.group_multi_operating_unit").id + ), + Command.link(cls.env.ref("account.group_account_invoice").id), + ], + "assigned_operating_unit_ids": [(6, 0, [cls.b2b.id])], + "default_operating_unit_id": cls.b2b.id, + "company_id": cls.company.id, + "company_ids": [Command.link(cls.company.id)], + } + ) + + Journal = cls.env["account.journal"].sudo() + cls.purchase_journal_b2b = Journal.create( + { + "name": "Vendor Bills B2B (test_pay_reg)", + "code": "TPRPB", + "type": "purchase", + "company_id": cls.company.id, + "operating_unit_id": cls.b2b.id, + } + ) + cls.cash_journal_ou1 = Journal.create( + { + "name": "Cash OU1 (test_pay_reg)", + "code": "TPRC1", + "type": "cash", + "company_id": cls.company.id, + "operating_unit_id": cls.ou1.id, + } + ) + cls.cash_journal_b2b = Journal.create( + { + "name": "Cash B2B (test_pay_reg)", + "code": "TPRCB", + "type": "cash", + "company_id": cls.company.id, + "operating_unit_id": cls.b2b.id, + } + ) + cls.expense_account = cls.env["account.account"].search( + [ + ("account_type", "=", "expense"), + ("company_ids", "in", cls.company.ids), + ], + limit=1, + ) + + def test_payment_register_filters_journals_by_invoice_ou(self): + invoice = ( + self.env["account.move"] + .with_context(default_move_type="in_invoice") + .create( + { + "partner_id": self.partner1.id, + "operating_unit_id": self.b2b.id, + "invoice_date": "2026-01-01", + "journal_id": self.purchase_journal_b2b.id, + "invoice_line_ids": [ + ( + 0, + 0, + { + "name": "Line", + "quantity": 1, + "price_unit": 100.0, + "account_id": self.expense_account.id, + "tax_ids": [], + }, + ) + ], + } + ) + ) + invoice.action_post() + + wizard = ( + self.env["account.payment.register"] + .with_user(self.user1) + .with_context( + active_model="account.move", + active_ids=invoice.ids, + ) + .create({"journal_id": self.cash_journal_b2b.id}) + ) + available = wizard.available_journal_ids + self.assertIn( + self.cash_journal_b2b, + available, + "B2B cash journal must be available to pay a B2B invoice.", + ) + self.assertNotIn( + self.cash_journal_ou1, + available, + "OU1 cash journal must NOT be available when paying a " "B2B invoice.", + )