From 700e9df1c9154c12327b91a4bf9b5ab9953b7140 Mon Sep 17 00:00:00 2001 From: Jessy Pango Date: Wed, 24 Jun 2026 05:24:27 +0100 Subject: [PATCH 01/11] [FIX] resolve Odoo 19 warnings for osv and translation eval --- accounting_pdf_reports/models/account_move_line.py | 5 ++++- om_account_followup/wizard/followup_print.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/accounting_pdf_reports/models/account_move_line.py b/accounting_pdf_reports/models/account_move_line.py index c28d0689..1a5c51da 100644 --- a/accounting_pdf_reports/models/account_move_line.py +++ b/accounting_pdf_reports/models/account_move_line.py @@ -1,5 +1,8 @@ import ast -from odoo.osv import expression +import warnings +with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + from odoo.osv import expression from odoo import api, models, fields diff --git a/om_account_followup/wizard/followup_print.py b/om_account_followup/wizard/followup_print.py index b76ba33d..079e9bd1 100644 --- a/om_account_followup/wizard/followup_print.py +++ b/om_account_followup/wizard/followup_print.py @@ -31,7 +31,7 @@ def _get_followup(self): related='followup_id.company_id') email_conf = fields.Boolean('Send Email Confirmation') email_subject = fields.Char('Email Subject', size=64, - default=_('Invoices Reminder')) + default=lambda self: _('Invoices Reminder')) partner_lang = fields.Boolean( 'Send Email in Partner Language', default=True, help='Do not change message text, if you want to send email in ' From 15703a69407c4b163f30161ae4f6133e23957a0b Mon Sep 17 00:00:00 2001 From: Jessy Pango Date: Wed, 24 Jun 2026 05:37:07 +0100 Subject: [PATCH 02/11] [FIX] om_account_budget: use ORM _read_group instead of deprecated _where_calc --- om_account_budget/models/account_budget.py | 26 ++++++++-------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/om_account_budget/models/account_budget.py b/om_account_budget/models/account_budget.py index 72068795..3a4a327d 100644 --- a/om_account_budget/models/account_budget.py +++ b/om_account_budget/models/account_budget.py @@ -172,13 +172,8 @@ def _compute_practical_amount(self): if acc_ids: domain += [('general_account_id', 'in', acc_ids)] - where_query = analytic_line_obj._where_calc(domain) - analytic_line_obj._apply_ir_rules(where_query, 'read') - from_string, from_params = where_query.from_clause - where_string, where_params = where_query.where_clause - from_clause, where_clause, where_clause_params = from_string, where_string, from_params + where_params - - select = "SELECT SUM(amount) from " + from_clause + " where " + where_clause + result = analytic_line_obj._read_group(domain, aggregates=['amount:sum']) + line.practical_amount = result[0][0] if result and result[0] else 0.0 else: aml_obj = self.env['account.move.line'] @@ -187,16 +182,13 @@ def _compute_practical_amount(self): ('date', '>=', date_from), ('date', '<=', date_to) ] - where_query = aml_obj._where_calc(domain) - aml_obj._apply_ir_rules(where_query, 'read') - from_string, from_params = where_query.from_clause - where_string, where_params = where_query.where_clause - from_clause, where_clause, where_clause_params = from_string, where_string, from_params + where_params - - select = "SELECT sum(credit)-sum(debit) from " + from_clause + " where " + where_clause - - self.env.cr.execute(select, where_clause_params) - line.practical_amount = self.env.cr.fetchone()[0] or 0.0 + + result = aml_obj._read_group(domain, aggregates=['credit:sum', 'debit:sum']) + if result and result[0]: + credit_sum, debit_sum = result[0] + line.practical_amount = (credit_sum or 0.0) - (debit_sum or 0.0) + else: + line.practical_amount = 0.0 def _compute_theoritical_amount(self): # beware: 'today' variable is mocked in the python tests and thus, its implementation matter From d9f84812f1b57bc6dd4769b90f68c04120cfb1f4 Mon Sep 17 00:00:00 2001 From: Jessy Pango Date: Wed, 24 Jun 2026 09:23:35 +0100 Subject: [PATCH 03/11] [FIX] om_account_budget: prevent TypeError in _compute_theoritical_amount with missing dates --- om_account_budget/models/account_budget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/om_account_budget/models/account_budget.py b/om_account_budget/models/account_budget.py index 3a4a327d..74de200a 100644 --- a/om_account_budget/models/account_budget.py +++ b/om_account_budget/models/account_budget.py @@ -199,6 +199,8 @@ def _compute_theoritical_amount(self): theo_amt = 0.00 else: theo_amt = line.planned_amount + elif not line.date_from or not line.date_to: + theo_amt = 0.00 else: line_timedelta = line.date_to - line.date_from elapsed_timedelta = today - line.date_from From 81751e1e4ee5594473ea5b9ea985e886ba4f1e0c Mon Sep 17 00:00:00 2001 From: Jessy Pango Date: Wed, 24 Jun 2026 09:34:11 +0100 Subject: [PATCH 04/11] [ADD] om_account_budget: initial tests for budgetary positions --- om_account_budget/tests/__init__.py | 1 + .../tests/test_account_budget.py | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 om_account_budget/tests/__init__.py create mode 100644 om_account_budget/tests/test_account_budget.py diff --git a/om_account_budget/tests/__init__.py b/om_account_budget/tests/__init__.py new file mode 100644 index 00000000..ca1a742e --- /dev/null +++ b/om_account_budget/tests/__init__.py @@ -0,0 +1 @@ +from . import test_account_budget diff --git a/om_account_budget/tests/test_account_budget.py b/om_account_budget/tests/test_account_budget.py new file mode 100644 index 00000000..da53ec45 --- /dev/null +++ b/om_account_budget/tests/test_account_budget.py @@ -0,0 +1,40 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError + +class TestAccountBudget(TransactionCase): + + @classmethod + def setUpClass(cls): + super(TestAccountBudget, cls).setUpClass() + cls.account_model = cls.env['account.account'] + cls.budget_post_model = cls.env['account.budget.post'] + + # Create a test account + cls.test_account = cls.account_model.create({ + 'name': 'Test Account', + 'code': '123456', + 'account_type': 'expense', + }) + + def test_01_budget_post_creation(self): + """Test the creation and validation of a budgetary position.""" + + # Scenario 1.1: Create without accounts should fail + with self.assertRaises(ValidationError): + self.budget_post_model.create({ + 'name': 'Empty Budget Post', + 'account_ids': [(5, 0, 0)] + }) + + # Scenario 1.2: Create with accounts should succeed + budget_post = self.budget_post_model.create({ + 'name': 'Valid Budget Post', + 'account_ids': [(4, self.test_account.id)] + }) + self.assertTrue(budget_post.id) + self.assertEqual(len(budget_post.account_ids), 1) + + # Scenario 1.3: Remove accounts from existing should fail + with self.assertRaises(ValidationError): + budget_post.write({'account_ids': [(5, 0, 0)]}) + From 660407df8de573dec094af4b7f2ba3f3f37f482c Mon Sep 17 00:00:00 2001 From: Jessy Pango Date: Wed, 24 Jun 2026 09:35:20 +0100 Subject: [PATCH 05/11] [ADD] om_account_budget: tests for budget state transitions --- .../tests/test_account_budget.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/om_account_budget/tests/test_account_budget.py b/om_account_budget/tests/test_account_budget.py index da53ec45..59de46c7 100644 --- a/om_account_budget/tests/test_account_budget.py +++ b/om_account_budget/tests/test_account_budget.py @@ -8,6 +8,7 @@ def setUpClass(cls): super(TestAccountBudget, cls).setUpClass() cls.account_model = cls.env['account.account'] cls.budget_post_model = cls.env['account.budget.post'] + cls.budget_model = cls.env['crossovered.budget'] # Create a test account cls.test_account = cls.account_model.create({ @@ -38,3 +39,30 @@ def test_01_budget_post_creation(self): with self.assertRaises(ValidationError): budget_post.write({'account_ids': [(5, 0, 0)]}) + def test_02_budget_state_transitions(self): + """Test the workflow and state transitions of a budget.""" + budget = self.budget_model.create({ + 'name': 'Test Budget', + 'date_from': '2026-01-01', + 'date_to': '2026-12-31', + }) + # Scenario 2.1: Created budget is in draft state + self.assertEqual(budget.state, 'draft') + + # Scenario 2.2: Test workflow transitions + budget.action_budget_confirm() + self.assertEqual(budget.state, 'confirm') + + budget.action_budget_validate() + self.assertEqual(budget.state, 'validate') + + budget.action_budget_done() + self.assertEqual(budget.state, 'done') + + # Scenario 2.3: Test cancellation and reset to draft + budget.action_budget_cancel() + self.assertEqual(budget.state, 'cancel') + + budget.action_budget_draft() + self.assertEqual(budget.state, 'draft') + From dc1fe0e3a8f115e2be3eb4f2821123e97c617fa3 Mon Sep 17 00:00:00 2001 From: Jessy Pango Date: Wed, 24 Jun 2026 09:36:07 +0100 Subject: [PATCH 06/11] [ADD] om_account_budget: tests for budget line constraints --- .../tests/test_account_budget.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/om_account_budget/tests/test_account_budget.py b/om_account_budget/tests/test_account_budget.py index 59de46c7..d1ee438f 100644 --- a/om_account_budget/tests/test_account_budget.py +++ b/om_account_budget/tests/test_account_budget.py @@ -66,3 +66,56 @@ def test_02_budget_state_transitions(self): budget.action_budget_draft() self.assertEqual(budget.state, 'draft') + def test_03_budget_lines_constraints(self): + """Test constraints on budget lines (missing targets, dates outside budget).""" + budget = self.budget_model.create({ + 'name': 'Test Budget 2', + 'date_from': '2026-01-01', + 'date_to': '2026-12-31', + }) + budget_line_model = self.env['crossovered.budget.lines'] + + # Scenario 3.1: Line without budget position and analytic account should fail + with self.assertRaises(ValidationError): + budget_line_model.create({ + 'crossovered_budget_id': budget.id, + 'date_from': '2026-01-01', + 'date_to': '2026-12-31', + 'planned_amount': 1000, + }) + + # Create valid budget post + budget_post = self.budget_post_model.create({ + 'name': 'Valid Post for Lines', + 'account_ids': [(4, self.test_account.id)] + }) + + # Scenario 3.2: Dates outside budget period + with self.assertRaises(ValidationError): + budget_line_model.create({ + 'crossovered_budget_id': budget.id, + 'general_budget_id': budget_post.id, + 'date_from': '2025-12-01', # Before budget start + 'date_to': '2026-12-31', + 'planned_amount': 1000, + }) + + with self.assertRaises(ValidationError): + budget_line_model.create({ + 'crossovered_budget_id': budget.id, + 'general_budget_id': budget_post.id, + 'date_from': '2026-01-01', + 'date_to': '2027-01-31', # After budget end + 'planned_amount': 1000, + }) + + # Scenario 3.3: Valid dates and targets should succeed + line = budget_line_model.create({ + 'crossovered_budget_id': budget.id, + 'general_budget_id': budget_post.id, + 'date_from': '2026-01-01', + 'date_to': '2026-12-31', + 'planned_amount': 1000, + }) + self.assertTrue(line.id) + From 2a5ea3f4c7d0300b5c1a6a14710e784d971724d0 Mon Sep 17 00:00:00 2001 From: Jessy Pango Date: Wed, 24 Jun 2026 09:36:53 +0100 Subject: [PATCH 07/11] [FIX] om_account_budget: trigger constrain for missing targets on create --- om_account_budget/models/account_budget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/om_account_budget/models/account_budget.py b/om_account_budget/models/account_budget.py index 74de200a..7a10ac69 100644 --- a/om_account_budget/models/account_budget.py +++ b/om_account_budget/models/account_budget.py @@ -222,7 +222,7 @@ def _compute_percentage(self): else: line.percentage = 0.00 - @api.constrains('general_budget_id', 'analytic_account_id') + @api.constrains('general_budget_id', 'analytic_account_id', 'crossovered_budget_id') def _must_have_analytical_or_budgetary_or_both(self): if not self.analytic_account_id and not self.general_budget_id: raise ValidationError( From faba92be7131325ad2b04a2dc3af81c5b2b19706 Mon Sep 17 00:00:00 2001 From: Jessy Pango Date: Wed, 24 Jun 2026 09:49:47 +0100 Subject: [PATCH 08/11] [ADD] om_account_budget: tests for theoretical and practical amounts --- .../tests/test_account_budget.py | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/om_account_budget/tests/test_account_budget.py b/om_account_budget/tests/test_account_budget.py index d1ee438f..6a824966 100644 --- a/om_account_budget/tests/test_account_budget.py +++ b/om_account_budget/tests/test_account_budget.py @@ -119,3 +119,105 @@ def test_03_budget_lines_constraints(self): }) self.assertTrue(line.id) + def test_04_theoretical_amount(self): + """Test the computation of theoretical amount based on elapsed days.""" + from unittest.mock import patch + from odoo import fields + + budget = self.budget_model.create({ + 'name': 'Theo Budget', + 'date_from': '2026-01-01', + 'date_to': '2026-12-31', + }) + budget_post = self.budget_post_model.create({ + 'name': 'Theo Post', + 'account_ids': [(4, self.test_account.id)] + }) + + line = self.env['crossovered.budget.lines'].create({ + 'crossovered_budget_id': budget.id, + 'general_budget_id': budget_post.id, + 'date_from': '2026-01-01', + 'date_to': '2026-12-31', + 'planned_amount': 3650, # 10/day for non-leap year 365 days + }) + + # Scenario 4.1: Today is before date_from + with patch('odoo.addons.om_account_budget.models.account_budget.fields.Date.today', return_value=fields.Date.to_date('2025-12-31')): + line._compute_theoritical_amount() + self.assertEqual(line.theoritical_amount, 0.0) + + # Scenario 4.2: Today is in the middle + # 2026-01-31 is 30 days elapsed since 2026-01-01 (01-01 to 01-31 is 30 days diff) + with patch('odoo.addons.om_account_budget.models.account_budget.fields.Date.today', return_value=fields.Date.to_date('2026-01-31')): + line._compute_theoritical_amount() + self.assertEqual(line.theoritical_amount, 300.0) # 30 days * 10/day + + # Scenario 4.3: Today is after date_to + with patch('odoo.addons.om_account_budget.models.account_budget.fields.Date.today', return_value=fields.Date.to_date('2027-01-01')): + line._compute_theoritical_amount() + self.assertEqual(line.theoritical_amount, 3650.0) + + # Scenario 4.4: Paid date set + line.paid_date = '2026-06-30' + # Today is before paid_date -> 0 + with patch('odoo.addons.om_account_budget.models.account_budget.fields.Date.today', return_value=fields.Date.to_date('2026-05-01')): + line._compute_theoritical_amount() + self.assertEqual(line.theoritical_amount, 0.0) + + # Today is after paid_date -> full planned_amount + with patch('odoo.addons.om_account_budget.models.account_budget.fields.Date.today', return_value=fields.Date.to_date('2026-07-01')): + line._compute_theoritical_amount() + self.assertEqual(line.theoritical_amount, 3650.0) + + def test_05_practical_amount(self): + """Test the computation of practical amount from account move lines.""" + budget = self.budget_model.create({ + 'name': 'Practical Budget', + 'date_from': '2026-01-01', + 'date_to': '2026-12-31', + }) + budget_post = self.budget_post_model.create({ + 'name': 'Practical Post', + 'account_ids': [(4, self.test_account.id)] + }) + + line = self.env['crossovered.budget.lines'].create({ + 'crossovered_budget_id': budget.id, + 'general_budget_id': budget_post.id, + 'date_from': '2026-01-01', + 'date_to': '2026-12-31', + 'planned_amount': 1000, + }) + + # Create account move within the dates + move = self.env['account.move'].create({ + 'move_type': 'entry', + 'date': '2026-06-15', + 'line_ids': [ + (0, 0, { + 'account_id': self.test_account.id, + 'debit': 500.0, + 'credit': 0.0, + 'name': 'Test Expense', + }), + (0, 0, { + 'account_id': self.env.company.account_journal_suspense_account_id.id or self.env['account.account'].search([], limit=1).id, + 'debit': 0.0, + 'credit': 500.0, + 'name': 'Test Credit', + }), + ] + }) + move.action_post() + + line._compute_practical_amount() + # practical_amount is credit - debit. Since we debited 500 on test_account, it's -500 + self.assertEqual(line.practical_amount, -500.0) + + line._compute_theoritical_amount() + line._compute_percentage() + # if theoretical != 0, percentage is practical / theoretical + # We don't assert the exact percentage because it depends on today's date during test run, + # but we can test if it doesn't crash. + self.assertIsNotNone(line.percentage) From 61b5cc65f510050c7f3d34fe34c377f09e774776 Mon Sep 17 00:00:00 2001 From: Jessy Pango Date: Wed, 24 Jun 2026 10:02:57 +0100 Subject: [PATCH 09/11] [FIX] om_account_budget: correct theoretical amount test assertion --- om_account_budget/tests/test_account_budget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/om_account_budget/tests/test_account_budget.py b/om_account_budget/tests/test_account_budget.py index 6a824966..2c8ea9d0 100644 --- a/om_account_budget/tests/test_account_budget.py +++ b/om_account_budget/tests/test_account_budget.py @@ -151,7 +151,7 @@ def test_04_theoretical_amount(self): # 2026-01-31 is 30 days elapsed since 2026-01-01 (01-01 to 01-31 is 30 days diff) with patch('odoo.addons.om_account_budget.models.account_budget.fields.Date.today', return_value=fields.Date.to_date('2026-01-31')): line._compute_theoritical_amount() - self.assertEqual(line.theoritical_amount, 300.0) # 30 days * 10/day + self.assertAlmostEqual(line.theoritical_amount, 300.82, places=2) # 30 / 364 * 3650 # Scenario 4.3: Today is after date_to with patch('odoo.addons.om_account_budget.models.account_budget.fields.Date.today', return_value=fields.Date.to_date('2027-01-01')): From 1059acf76b6b2b36a45827b78bbb211f1c564922 Mon Sep 17 00:00:00 2001 From: Jessy Pango Date: Wed, 24 Jun 2026 10:14:24 +0100 Subject: [PATCH 10/11] [ADD] om_account_budget: tests for security and multicompany access --- .../tests/test_account_budget.py | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/om_account_budget/tests/test_account_budget.py b/om_account_budget/tests/test_account_budget.py index 2c8ea9d0..95a4fee8 100644 --- a/om_account_budget/tests/test_account_budget.py +++ b/om_account_budget/tests/test_account_budget.py @@ -1,5 +1,5 @@ from odoo.tests.common import TransactionCase -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, AccessError class TestAccountBudget(TransactionCase): @@ -221,3 +221,59 @@ def test_05_practical_amount(self): # We don't assert the exact percentage because it depends on today's date during test run, # but we can test if it doesn't crash. self.assertIsNotNone(line.percentage) + + def test_06_security_access(self): + """Test that regular users cannot create budgets.""" + # Create a regular user + user = self.env['res.users'].create({ + 'name': 'Regular User', + 'login': 'regular_user_budget', + 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])] + }) + + # User without accounting rights should not be able to create a budget + with self.assertRaises(AccessError): + self.budget_model.with_user(user).create({ + 'name': 'Unauthorized Budget', + 'date_from': '2026-01-01', + 'date_to': '2026-12-31', + }) + + # Add accounting rights + user.groups_id = [(4, self.env.ref('account.group_account_user').id)] + + # Should now be able to create a budget + budget = self.budget_model.with_user(user).create({ + 'name': 'Authorized Budget', + 'date_from': '2026-01-01', + 'date_to': '2026-12-31', + }) + self.assertTrue(budget.id) + + def test_07_multicompany_access(self): + """Test multi-company security rules on budgets.""" + company_a = self.env['res.company'].create({'name': 'Company A'}) + company_b = self.env['res.company'].create({'name': 'Company B'}) + + budget_a = self.budget_model.create({ + 'name': 'Budget A', + 'date_from': '2026-01-01', + 'date_to': '2026-12-31', + 'company_id': company_a.id, + }) + + # Create a user in Company B + user_b = self.env['res.users'].create({ + 'name': 'User B', + 'login': 'user_b_budget', + 'company_id': company_b.id, + 'company_ids': [(6, 0, [company_b.id])], + 'groups_id': [(6, 0, [ + self.env.ref('base.group_user').id, + self.env.ref('account.group_account_user').id + ])] + }) + + # User B should not be able to see Budget A + budgets_visible = self.budget_model.with_user(user_b).search([('id', '=', budget_a.id)]) + self.assertFalse(budgets_visible) From e6e97b9e6bc28b23196f0a2ee9e4bd2089192291 Mon Sep 17 00:00:00 2001 From: Jessy Pango Date: Wed, 24 Jun 2026 10:16:56 +0100 Subject: [PATCH 11/11] [FIX] om_account_budget: correct group_ids field in tests --- om_account_budget/tests/test_account_budget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/om_account_budget/tests/test_account_budget.py b/om_account_budget/tests/test_account_budget.py index 95a4fee8..a5dae6c7 100644 --- a/om_account_budget/tests/test_account_budget.py +++ b/om_account_budget/tests/test_account_budget.py @@ -228,7 +228,7 @@ def test_06_security_access(self): user = self.env['res.users'].create({ 'name': 'Regular User', 'login': 'regular_user_budget', - 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])] + 'group_ids': [(6, 0, [self.env.ref('base.group_user').id])] }) # User without accounting rights should not be able to create a budget @@ -240,7 +240,7 @@ def test_06_security_access(self): }) # Add accounting rights - user.groups_id = [(4, self.env.ref('account.group_account_user').id)] + user.write({'group_ids': [(4, self.env.ref('account.group_account_user').id)]}) # Should now be able to create a budget budget = self.budget_model.with_user(user).create({ @@ -268,7 +268,7 @@ def test_07_multicompany_access(self): 'login': 'user_b_budget', 'company_id': company_b.id, 'company_ids': [(6, 0, [company_b.id])], - 'groups_id': [(6, 0, [ + 'group_ids': [(6, 0, [ self.env.ref('base.group_user').id, self.env.ref('account.group_account_user').id ])]