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/__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_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/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.", + ) 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