From 0236ef0809fc1ae39512133b2ee3cf59419b64bd Mon Sep 17 00:00:00 2001 From: git_admin Date: Tue, 28 Apr 2026 07:33:31 +0000 Subject: [PATCH] Tower: upload at_accounting 18.0.1.7 (via marketplace) --- .../tests/test_analytic_reports.py | 708 ++++++++++++++++++ 1 file changed, 708 insertions(+) create mode 100644 addons/at_accounting/tests/test_analytic_reports.py diff --git a/addons/at_accounting/tests/test_analytic_reports.py b/addons/at_accounting/tests/test_analytic_reports.py new file mode 100644 index 0000000..b623a0a --- /dev/null +++ b/addons/at_accounting/tests/test_analytic_reports.py @@ -0,0 +1,708 @@ +from odoo import Command +from odoo.tests import tagged + +from .common import TestAccountReportsCommon + + +@tagged('post_install', '-at_install') +class TestAnalyticReport(TestAccountReportsCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.env.user.groups_id += cls.env.ref( + 'analytic.group_analytic_accounting') + cls.report = cls.env.ref('at_accounting.profit_and_loss') + cls.report.write({'filter_analytic': True}) + + cls.analytic_plan_parent = cls.env['account.analytic.plan'].create({ + 'name': 'Plan Parent', + }) + cls.analytic_plan_child = cls.env['account.analytic.plan'].create({ + 'name': 'Plan Child', + 'parent_id': cls.analytic_plan_parent.id, + }) + + cls.analytic_account_parent = cls.env['account.analytic.account'].create({ + 'name': 'Account 1', + 'plan_id': cls.analytic_plan_parent.id + }) + cls.analytic_account_parent_2 = cls.env['account.analytic.account'].create({ + 'name': 'Account 2', + 'plan_id': cls.analytic_plan_parent.id + }) + cls.analytic_account_child = cls.env['account.analytic.account'].create({ + 'name': 'Account 3', + 'plan_id': cls.analytic_plan_child.id + }) + cls.analytic_account_parent_3 = cls.env['account.analytic.account'].create({ + 'name': 'Account 4', + 'plan_id': cls.analytic_plan_parent.id + }) + + def test_report_group_by_analytic_plan(self): + + out_invoice = self.env['account.move'].create([{ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'date': '2019-05-01', + 'invoice_date': '2019-05-01', + 'invoice_line_ids': [ + Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 200.0, + 'analytic_distribution': { + self.analytic_account_parent.id: 100, + }, + }), + Command.create({ + 'product_id': self.product_b.id, + 'price_unit': 200.0, + 'analytic_distribution': { + self.analytic_account_child.id: 100, + }, + }), + ] + }]) + out_invoice.action_post() + + options = self._generate_options( + self.report, + '2019-01-01', + '2019-12-31', + default_options={ + 'analytic_plans_groupby': [self.analytic_plan_parent.id, self.analytic_plan_child.id], + } + ) + + lines = self.report._get_lines(options) + + self.assertLinesValues( + # pylint: disable=bad-whitespace + lines, + [ 0, 1, 2], + [ + ['Revenue', 400.00, 200.00], + ['Less Costs of Revenue', 0.00, 0.00], + ['Gross Profit', 400.00, 200.00], + ['Less Operating Expenses', 0.00, 0.00], + ['Operating Income (or Loss)', 400.00, 200.00], + ['Plus Other Income', 0.00, 0.00], + ['Less Other Expenses', 0.00, 0.00], + ['Net Profit', 400.00, 200.00], + ], + options, + currency_map={ + 1: {'currency': self.env.company.currency_id}, + 2: {'currency': self.env.company.currency_id}, + }, + ) + + def test_report_analytic_filter(self): + + out_invoice = self.env['account.move'].create([{ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'date': '2023-02-01', + 'invoice_date': '2023-02-01', + 'invoice_line_ids': [ + Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 1000.0, + 'analytic_distribution': { + self.analytic_account_parent.id: 100, + }, + }) + ] + }]) + out_invoice.action_post() + + options = self._generate_options( + self.report, + '2023-01-01', + '2023-12-31', + default_options={ + 'analytic_accounts': [self.analytic_account_parent.id], + } + ) + + self.assertLinesValues( + # pylint: disable=C0326 + # pylint: disable=bad-whitespace + self.report._get_lines(options), + [ 0, 1], + [ + ['Revenue', 1000.00], + ['Less Costs of Revenue', 0.00], + ['Gross Profit', 1000.00], + ['Less Operating Expenses', 0.00], + ['Operating Income (or Loss)', 1000.00], + ['Plus Other Income', 0.00], + ['Less Other Expenses', 0.00], + ['Net Profit', 1000.00], + ], + options, + currency_map={ + 1: {'currency': self.env.company.currency_id}, + 2: {'currency': self.env.company.currency_id}, + }, + ) + + # Set the unused analytic account in filter, as no move is + # using this account, the column should be empty + options['analytic_accounts'] = [self.analytic_account_child.id] + + self.assertLinesValues( + # pylint: disable=C0326 + # pylint: disable=bad-whitespace + self.report._get_lines(options), + [ 0, 1], + [ + ['Revenue', 0.00], + ['Less Costs of Revenue', 0.00], + ['Gross Profit', 0.00], + ['Less Operating Expenses', 0.00], + ['Operating Income (or Loss)', 0.00], + ['Plus Other Income', 0.00], + ['Less Other Expenses', 0.00], + ['Net Profit', 0.00], + ], + options, + currency_map={ + 1: {'currency': self.env.company.currency_id}, + 2: {'currency': self.env.company.currency_id}, + }, + ) + + def test_report_audit_analytic_filter(self): + out_invoice = self.env['account.move'].create([{ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'date': '2023-02-01', + 'invoice_date': '2023-02-01', + 'invoice_line_ids': [ + Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 1000.0, + 'analytic_distribution': { + self.analytic_account_parent.id: 100, + }, + }), + Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 500.0, + 'analytic_distribution': { + self.analytic_account_child.id: 100, + }, + }), + ], + }]) + out_invoice.action_post() + + options = self._generate_options( + self.report, + '2023-01-01', + '2023-12-31', + default_options={ + 'analytic_accounts': [self.analytic_account_parent.id], + } + ) + + lines = self.report._get_lines(options) + + report_line = self.report.line_ids[0] + report_line_dict = next(x for x in lines if x['name'] == report_line.name) + + action_dict = self.report.action_audit_cell( + options, + self._get_audit_params_from_report_line(options, report_line, report_line_dict), + ) + + audited_lines = self.env['account.move.line'].search(action_dict['domain']) + self.assertEqual(audited_lines, out_invoice.invoice_line_ids[0], "Only the line with the parent account should be shown") + + def test_report_analytic_groupby_and_filter(self): + """ + Test that the analytic filter is applied on the groupby columns + """ + + out_invoice = self.env['account.move'].create([{ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'date': '2023-02-01', + 'invoice_date': '2023-02-01', + 'invoice_line_ids': [ + Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 1000.0, + 'analytic_distribution': { + self.analytic_account_parent.id: 40, + self.analytic_account_child.id: 60, + }, + }) + ] + }]) + out_invoice.action_post() + + # Test with only groupby + options = self._generate_options( + self.report, + '2023-01-01', + '2023-12-31', + default_options={ + 'analytic_accounts_groupby': [self.analytic_account_parent.id, self.analytic_account_child.id], + } + ) + + self.assertLinesValues( + # pylint: disable=C0326 + # pylint: disable=bad-whitespace + self.report._get_lines(options), + [ 0, 1, 2, 3], + [ + ['Revenue', 400.00, 600.00, 1000.00], + ['Less Costs of Revenue', 0.00, 0.00, 0.00], + ['Gross Profit', 400.00, 600.00, 1000.00], + ['Less Operating Expenses', 0.00, 0.00, 0.00], + ['Operating Income (or Loss)', 400.00, 600.00, 1000.00], + ['Plus Other Income', 0.00, 0.00, 0.00], + ['Less Other Expenses', 0.00, 0.00, 0.00], + ['Net Profit', 400.00, 600.00, 1000.00], + ], + options, + currency_map={ + 1: {'currency': self.env.company.currency_id}, + 2: {'currency': self.env.company.currency_id}, + }, + ) + + # Adding analytic filter for the two analytic accounts used on the invoice line + # The two groupby columns should still be filled + options['analytic_accounts'] = [self.analytic_account_parent.id, self.analytic_account_child.id] + + self.assertLinesValues( + # pylint: disable=C0326 + # pylint: disable=bad-whitespace + self.report._get_lines(options), + [ 0, 1, 2, 3], + [ + ['Revenue', 400.00, 600.00, 1000.00], + ['Less Costs of Revenue', 0.00, 0.00, 0.00], + ['Gross Profit', 400.00, 600.00, 1000.00], + ['Less Operating Expenses', 0.00, 0.00, 0.00], + ['Operating Income (or Loss)', 400.00, 600.00, 1000.00], + ['Plus Other Income', 0.00, 0.00, 0.00], + ['Less Other Expenses', 0.00, 0.00, 0.00], + ['Net Profit', 400.00, 600.00, 1000.00], + ], + options, + currency_map={ + 1: {'currency': self.env.company.currency_id}, + 2: {'currency': self.env.company.currency_id}, + }, + ) + # Keep only first analytic account on filter, the groupby column + # for this account should still be filled, unlike the other + options['analytic_accounts'] = [self.analytic_account_parent.id] + + self.assertLinesValues( + # pylint: disable=C0326 + # pylint: disable=bad-whitespace + self.report._get_lines(options), + [ 0, 1, 2, 3], + [ + ['Revenue', 400.00, 0.00, 1000.00], + ['Less Costs of Revenue', 0.00, 0.00, 0.00], + ['Gross Profit', 400.00, 0.00, 1000.00], + ['Less Operating Expenses', 0.00, 0.00, 0.00], + ['Operating Income (or Loss)', 400.00, 0.00, 1000.00], + ['Plus Other Income', 0.00, 0.00, 0.00], + ['Less Other Expenses', 0.00, 0.00, 0.00], + ['Net Profit', 400.00, 0.00, 1000.00], + ], + options, + currency_map={ + 1: {'currency': self.env.company.currency_id}, + 2: {'currency': self.env.company.currency_id}, + }, + ) + + # Keep only first analytic account on filter, the groupby column + # for this account should still be filled, unlike the other + options['analytic_accounts'] = [self.analytic_account_child.id] + + self.assertLinesValues( + # pylint: disable=C0326 + # pylint: disable=bad-whitespace + self.report._get_lines(options), + [ 0, 1, 2, 3], + [ + ['Revenue', 0.00, 600.00, 1000.00], + ['Less Costs of Revenue', 0.00, 0.00, 0.00], + ['Gross Profit', 0.00, 600.00, 1000.00], + ['Less Operating Expenses', 0.00, 0.00, 0.00], + ['Operating Income (or Loss)', 0.00, 600.00, 1000.00], + ['Plus Other Income', 0.00, 0.00, 0.00], + ['Less Other Expenses', 0.00, 0.00, 0.00], + ['Net Profit', 0.00, 600.00, 1000.00], + ], + options, + currency_map={ + 1: {'currency': self.env.company.currency_id}, + 2: {'currency': self.env.company.currency_id}, + }, + ) + + # Set an unused analytic account in filter, all the columns + # should be empty, as no move is using this account + options['analytic_accounts'] = [self.analytic_account_parent_2.id] + + self.assertLinesValues( + # pylint: disable=C0326 + # pylint: disable=bad-whitespace + self.report._get_lines(options), + [ 0, 1, 2, 3], + [ + ['Revenue', 0.00, 0.00, 0.00], + ['Less Costs of Revenue', 0.00, 0.00, 0.00], + ['Gross Profit', 0.00, 0.00, 0.00], + ['Less Operating Expenses', 0.00, 0.00, 0.00], + ['Operating Income (or Loss)', 0.00, 0.00, 0.00], + ['Plus Other Income', 0.00, 0.00, 0.00], + ['Less Other Expenses', 0.00, 0.00, 0.00], + ['Net Profit', 0.00, 0.00, 0.00], + ], + options, + currency_map={ + 1: {'currency': self.env.company.currency_id}, + 2: {'currency': self.env.company.currency_id}, + }, + ) + + def test_audit_cell_analytic_groupby_and_filter(self): + """ + Test that the analytic filters are applied on the auditing of the cells + """ + def _get_action_dict(options, column_index): + lines = self.report._get_lines(options) + report_line = self.report.line_ids[0] + report_line_dict = next(x for x in lines if x['name'] == report_line.name) + audit_param = self._get_audit_params_from_report_line(options, report_line, report_line_dict, column_group_key=list(options['column_groups'])[column_index]) + return self.report.action_audit_cell(options, audit_param) + + other_plan = self.env['account.analytic.plan'].create({'name': "Other Plan"}) + other_account = self.env['account.analytic.account'].create({'name': "Other Account", 'plan_id': other_plan.id, 'active': True}) + + out_invoices = self.env['account.move'].create([ + { + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'date': '2023-02-01', + 'invoice_date': '2023-02-01', + 'invoice_line_ids': [ + Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 1000.0, + 'analytic_distribution': { + self.analytic_account_parent.id: 40, + self.analytic_account_child.id: 60, + } + }), + ] + }, + { + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'date': '2023-02-01', + 'invoice_date': '2023-02-01', + 'invoice_line_ids': [ + Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 2000.0, + 'analytic_distribution': { + f'{self.analytic_account_parent.id},{other_account.id}': 100, + }, + }), + ] + } + ]) + out_invoices.action_post() + out_invoices = out_invoices.with_context(analytic_plan_id=self.analytic_plan_parent.id) + analytic_lines_parent = out_invoices.invoice_line_ids.analytic_line_ids.filtered(lambda line: line.auto_account_id == self.analytic_account_parent) + analytic_lines_other = out_invoices.with_context(analytic_plan_id=other_plan.id).invoice_line_ids.analytic_line_ids.filtered(lambda line: line.auto_account_id == other_account) + + # Test with only groupby + options = self._generate_options( + self.report, + '2023-01-01', + '2023-12-31', + default_options={ + 'analytic_accounts_groupby': [self.analytic_account_parent.id, other_account.id], + } + ) + action_dict = _get_action_dict(options, 0) # First Column => Parent + self.assertEqual( + self.env['account.analytic.line'].search(action_dict['domain']), + analytic_lines_parent, + "Only the Analytic Line related to the Parent should be shown", + ) + action_dict = _get_action_dict(options, 1) # Second Column => Other + self.assertEqual( + self.env['account.analytic.line'].search(action_dict['domain']), + analytic_lines_other, + "Only the Analytic Line related to the Parent should be shown", + ) + + action_dict = _get_action_dict(options, 2) # Third Column => AMLs + self.assertEqual( + out_invoices.line_ids.filtered_domain(action_dict['domain']), + out_invoices.invoice_line_ids, + "Both amls should be shown", + ) + + # Adding analytic filter for the two analytic accounts used on the invoice line + options['analytic_accounts'] = [self.analytic_account_parent.id, other_account.id] + action_dict = _get_action_dict(options, 0) # First Column => Parent + self.assertEqual( + self.env['account.analytic.line'].search(action_dict['domain']), + analytic_lines_parent, + "Still only the Analytic Line related to the Parent should be shown", + ) + action_dict = _get_action_dict(options, 1) # Second Column => Other + self.assertEqual( + self.env['account.analytic.line'].search(action_dict['domain']), + analytic_lines_other, + "Still only the Analytic Line related to the Parent should be shown", + ) + + action_dict = _get_action_dict(options, 2) # Third Column => AMLs + self.assertEqual( + out_invoices.line_ids.search(action_dict['domain']), + out_invoices.invoice_line_ids, + "Both amls should be shown", + ) + + def test_general_ledger_analytic_filter(self): + analytic_plan = self.env["account.analytic.plan"].create({ + "name": "Default Plan", + }) + analytic_account = self.env["account.analytic.account"].create({ + "name": "Test Account", + "plan_id": analytic_plan.id, + }) + + invoice = self.init_invoice( + "out_invoice", + amounts=[100, 200], + invoice_date="2023-01-01", + ) + invoice.action_post() + invoice.invoice_line_ids[0].analytic_distribution = {analytic_account.id: 100} + + general_ledger_report = self.env.ref("at_accounting.general_ledger_report") + options = self._generate_options( + general_ledger_report, + "2023-01-01", + "2023-01-01", + default_options={ + 'analytic_accounts': [analytic_account.id], + 'unfold_all': True, + } + ) + + self.assertLinesValues( + general_ledger_report._get_lines(options), + # Name Debit Credit Balance + [ 0, 5, 6, 7], + [ + ['400000 Product Sales', 0.00, 100.00, -100.00], + ['INV/2023/00001', 0.00, 100.00, -100.00], + ['Total 400000 Product Sales', 0.00, 100.00, -100.00], + ['Total', 0.00, 100.00, -100.00], + ], + options, + ) + + def test_analytic_groupby_with_horizontal_groupby(self): + + out_invoice_1 = self.env['account.move'].create([{ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'date': '2024-07-01', + 'invoice_date': '2024-07-01', + 'invoice_line_ids': [ + Command.create({ + 'product_id': self.product_b.id, + 'price_unit': 500.0, + 'analytic_distribution': { + self.analytic_account_parent_2.id: 80, + self.analytic_account_parent_3.id: -10, + }, + }), + ] + }]) + out_invoice_1.action_post() + + out_invoice_2 = self.env['account.move'].create([{ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'date': '2024-07-01', + 'invoice_date': '2024-07-01', + 'invoice_line_ids': [ + Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 100.0, + 'analytic_distribution': { + self.analytic_account_parent.id: 100, + }, + }), + ] + }]) + out_invoice_2.action_post() + + horizontal_group = self.env['account.report.horizontal.group'].create({ + 'name': 'Horizontal Group Journal Entries', + 'report_ids': [self.report.id], + 'rule_ids': [ + Command.create({ + 'field_name': 'move_id', # this field is specific to account.move.line and not in account.analytic.line + 'domain': f"[('id', 'in', {(out_invoice_1 + out_invoice_2).ids})]", + }), + ], + }) + + options = self._generate_options( + self.report, + '2024-01-01', + '2024-12-31', + default_options={ + 'analytic_accounts_groupby': [self.analytic_account_parent.id, self.analytic_account_parent_2.id, self.analytic_account_parent_3.id], + 'selected_horizontal_group_id': horizontal_group.id, + } + ) + + self.assertLinesValues( + self.report._get_lines(options), + # Horizontal groupby [ Move 2 ] [ Move 1 ] + # Analytic groupby A1 A2 A3 Balance A1 A2 A3 Balance + [ 0, 1, 2, 3, 4, 5, 6, 7, 8], + [ + ['Revenue', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00], + ['Less Costs of Revenue', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + ['Gross Profit', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00], + ['Less Operating Expenses', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + ['Operating Income (or Loss)', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00], + ['Plus Other Income', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + ['Less Other Expenses', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + ['Net Profit', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00], + ], + options, + ) + + def test_analytic_groupby_with_analytic_simulations(self): + """ + Create an analytic simulation (analytic line without a move line) + and check that it is taken into account in the report + """ + + self.env['account.analytic.line'].create({ + 'name': 'Simulation', + 'date': '2019-05-01', + 'amount': 100.0, + 'unit_amount': 1.0, + 'company_id': self.env.company.id, + self.analytic_plan_parent._column_name(): self.analytic_account_parent.id, + 'general_account_id': self.company_data['default_account_revenue'].id, + }) + + options = self._generate_options( + self.report, + '2019-01-01', + '2019-12-31', + default_options={ + 'analytic_plans_groupby': [self.analytic_plan_parent.id, self.analytic_plan_child.id], + 'include_analytic_without_aml': True, + } + ) + + self.assertLinesValues( + self.report._get_lines(options), + [ 0, 1, 2], + [ + ('Revenue', 100.00, 0.00), + ('Less Costs of Revenue', 0.00, 0.00), + ('Gross Profit', 100.00, 0.00), + ('Less Operating Expenses', 0.00, 0.00), + ('Operating Income (or Loss)', 100.00, 0.00), + ('Plus Other Income', 0.00, 0.00), + ('Less Other Expenses', 0.00, 0.00), + ('Net Profit', 100.00, 0.00), + ], + options, + ) + + def test_analytic_groupby_plans_without_analytic_accounts(self): + """ + Ensure that grouping on several analytic plans without any analytic accounts works as expected + """ + analytic_plans_without_accounts = self.env['account.analytic.plan'].create([ + {'name': 'Plan 1'}, + {'name': 'Plan 2'}, + ]) + + options = self._generate_options( + self.report, '2019-01-01', '2019-12-31', + default_options={'analytic_plans_groupby': analytic_plans_without_accounts.ids} + ) + + self.assertEqual( + len(options['column_groups']), 3, + "the number of column groups should be 3, despite the 2 analytic plans having the exact same analytic accounts list" + ) + + self.assertLinesValues( + self.report._get_lines(options), + # Plan 1 Plan 2 Total + [ 0, 1, 2, 3], + [ + ('Revenue', 0.00, 0.00, 0.00), + ('Less Costs of Revenue', 0.00, 0.00, 0.00), + ('Gross Profit', 0.00, 0.00, 0.00), + ('Less Operating Expenses', 0.00, 0.00, 0.00), + ('Operating Income (or Loss)', 0.00, 0.00, 0.00), + ('Plus Other Income', 0.00, 0.00, 0.00), + ('Less Other Expenses', 0.00, 0.00, 0.00), + ('Net Profit', 0.00, 0.00, 0.00), + ], + options, + ) + + def test_profit_and_loss_multicompany_access_rights(self): + branch = self.env['res.company'].create([{ + 'name': "My Test Branch", + 'parent_id': self.env.company.id, + }]) + other_currency = self.setup_other_currency('EUR', rounding=0.001) + test_journal = self.env['account.journal'].create({ + 'name': 'Test Journal', + 'code': 'TEST', + 'type': 'sale', + 'company_id': self.env.company.id, + 'currency_id': other_currency.id, + }) + test_user = self.env['res.users'].create({ + 'login': 'test', + 'name': 'The King', + 'email': 'noop@example.com', + 'groups_id': [Command.link(self.env.ref('account.group_account_manager').id)], + 'company_ids': [Command.link(self.env.company.id), Command.link(branch.id)], + }) + self.env.invalidate_all() + + options = self._generate_options( + self.report.with_user(test_user).with_company(branch), '2019-01-01', '2019-12-31', + ) + lines = self.report._get_lines(options) + self.assertTrue(lines) + self.assertEqual(test_journal.display_name, "Test Journal (EUR)")