Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions account_operating_unit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

from . import models
from . import report
from . import wizard
1 change: 1 addition & 0 deletions account_operating_unit/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
41 changes: 41 additions & 0 deletions account_operating_unit/tests/test_account_operating_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry for the nitpicking, can you remove all the empty lines inside the method?

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()
151 changes: 151 additions & 0 deletions account_operating_unit/tests/test_payment_register_journal_ou.py
Original file line number Diff line number Diff line change
@@ -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.",
)
1 change: 1 addition & 0 deletions account_operating_unit/wizard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import account_payment_register
21 changes: 21 additions & 0 deletions account_operating_unit/wizard/account_payment_register.py
Original file line number Diff line number Diff line change
@@ -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
Loading