From cf8627605f224b63c00543782271f22017b2ab7f Mon Sep 17 00:00:00 2001 From: gawa-odoo Date: Mon, 2 Jan 2023 13:48:39 +0000 Subject: [PATCH] [FIX] account: update fiscal position when updating taxes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fiscal positions for the new taxes should also be provided, when we create the taxes, so the clients don't have to manually input them. Only fiscal positions that exist in the db and with new taxes are created. Related: https://github.com/odoo/odoo/pull/108571 task-3116246 closes odoo/odoo#109491 X-original-commit: b6947f268df6645ebcf6e7538f82bc5b535a38c5 Signed-off-by: William André (wan) Signed-off-by: Wala Gauthier (gawa) --- addons/account/models/chart_template.py | 42 +++- addons/account/tests/__init__.py | 1 + addons/account/tests/test_chart_template.py | 215 ++++++++++++++++++++ 3 files changed, 249 insertions(+), 9 deletions(-) create mode 100644 addons/account/tests/test_chart_template.py diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 2ceecf27717dc..83e15dc4171c5 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -61,9 +61,9 @@ def _update_tax_from_template(template, tax): tax_line.write({"tag_ids": [(6, 0, tags_to_add.ids)]}) _cleanup_tags(tags_to_unlink) - def _get_template_to_tax_xmlid_mapping(company): + def _get_template_to_real_xmlid_mapping(company, model): """ - This function uses ir_model_data to return a mapping between the tax templates and the taxes, using their xmlid + This function uses ir_model_data to return a mapping between the templates and the data, using their xmlid :returns: { account.tax.template.id: account.tax.id } @@ -72,15 +72,15 @@ def _get_template_to_tax_xmlid_mapping(company): env.cr.execute( """ SELECT template.res_id AS template_res_id, - tax.res_id AS tax_res_id - FROM ir_model_data tax + data.res_id AS data_res_id + FROM ir_model_data data JOIN ir_model_data template - ON template.name = substr(tax.name, strpos(tax.name, '_') + 1) - WHERE tax.model = 'account.tax' - AND tax.name LIKE %s + ON template.name = substr(data.name, strpos(data.name, '_') + 1) + WHERE data.model = %s + AND data.name LIKE %s -- tax.name is of the form: {company_id}_{account.tax.template.name} """, - [r"%s\_%%" % company.id], + [model, r"%s\_%%" % company.id], ) tuples = env.cr.fetchall() return dict(tuples) @@ -116,6 +116,26 @@ def _cleanup_tags(tags): if not (aml_using_tag or tax_using_tag or report_line_using_tag): tag.unlink() + def _update_fiscal_positions_from_templates(company, chart_template_id, new_taxes_template): + chart_template = env["account.chart.template"].browse(chart_template_id) + positions = env['account.fiscal.position.template'].search([('chart_template_id', '=', chart_template_id)]) + tax_template_ref = _get_template_to_real_xmlid_mapping(company, 'account.tax') + fp_template_ref = _get_template_to_real_xmlid_mapping(company, 'account.fiscal.position') + + tax_template_vals = [] + for position_template in positions: + fp = env["account.fiscal.position"].browse(fp_template_ref.get(position_template.id)) + if not fp: + continue + for position_tax in position_template.tax_ids: + if position_tax.tax_src_id in new_taxes_template or position_tax.tax_dest_id in new_taxes_template: + tax_template_vals.append((position_tax, { + 'tax_src_id': tax_template_ref[position_tax.tax_src_id.id], + 'tax_dest_id': position_tax.tax_dest_id and tax_template_ref[position_tax.tax_dest_id.id] or False, + 'position_id': fp.id, + })) + chart_template._create_records_with_xmlid('account.fiscal.position.tax', tax_template_vals, company) + def _notify_accountant_managers(taxes_to_check): accountant_manager_group = env.ref("account.group_account_manager") partner_managers_ids = accountant_manager_group.users.mapped('partner_id') @@ -142,8 +162,9 @@ def _notify_accountant_managers(taxes_to_check): chart_template_id = env['ir.model.data']._xmlid_to_res_id(chart_template_xmlid) companies = env['res.company'].search([('chart_template_id', '=', chart_template_id)]) outdated_taxes = [] + new_taxes_template = [] for company in companies: - template_to_tax = _get_template_to_tax_xmlid_mapping(company) + template_to_tax = _get_template_to_real_xmlid_mapping(company, 'account.tax') templates = env['account.tax.template'].with_context(active_test=False).search([("chart_template_id", "=", chart_template_id)]) for template in templates: tax = env["account.tax"].browse(template_to_tax.get(template.id)) @@ -151,8 +172,11 @@ def _notify_accountant_managers(taxes_to_check): _create_tax_from_template(company, template, old_tax=tax) if tax: outdated_taxes.append(tax) + else: + new_taxes_template.append(template) else: _update_tax_from_template(template, tax) + _update_fiscal_positions_from_templates(company, chart_template_id, new_taxes_template) if outdated_taxes: _notify_accountant_managers(outdated_taxes) diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index 538ccd751dcc5..267760e644f49 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -20,6 +20,7 @@ from . import test_account_invoice_report from . import test_account_move_line_tax_details from . import test_account_journal_dashboard +from . import test_chart_template from . import test_fiscal_position from . import test_reconciliation from . import test_sequence_mixin diff --git a/addons/account/tests/test_chart_template.py b/addons/account/tests/test_chart_template.py new file mode 100644 index 0000000000000..74511571fd439 --- /dev/null +++ b/addons/account/tests/test_chart_template.py @@ -0,0 +1,215 @@ +from odoo import Command +from odoo.addons.account.models.chart_template import update_taxes_from_templates +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + + +@tagged('post_install', '-at_install') +class TestChartTemplate(TransactionCase): + + @classmethod + def create_tax_template(cls, name, template_name, amount): + return cls.env['account.tax.template']._load_records([{ + 'xml_id': template_name, + 'values': { + 'name': name, + 'amount': amount, + 'chart_template_id': cls.chart_template.id, + 'invoice_repartition_line_ids': [ + Command.create({ + 'factor_percent': 100, + 'repartition_type': 'base', + }), + Command.create({ + 'factor_percent': 100, + 'repartition_type': 'tax', + }), + ], + 'refund_repartition_line_ids': [ + Command.create({ + 'factor_percent': 100, + 'repartition_type': 'base', + }), + Command.create({ + 'factor_percent': 100, + 'repartition_type': 'tax', + }), + ], + }, + }]) + + @classmethod + def setUpClass(cls): + """ + Setups a company with a custom chart template, containing a tax and a fiscal position. + We need to add xml_ids to the templates because they are loaded from their xml_ids + """ + super().setUpClass() + + # Create user. + user = cls.env['res.users'].create({ + 'name': 'Because I am accountman!', + 'login': 'accountman', + 'password': 'accountman', + 'groups_id': [Command.set(cls.env.user.groups_id.ids), Command.link(cls.env.ref('account.group_account_user').id)], + }) + user.partner_id.email = 'accountman@test.com' + + cls.company_1 = cls.env['res.company'].create({ + 'name': 'TestCompany1', + 'country_id': cls.env.ref('base.be').id, + }) + + cls.env = cls.env(user=user) + cls.cr = cls.env.cr + + user.write({ + 'company_ids': [Command.set(cls.company_1.ids)], + 'company_id': cls.company_1.id, + }) + + cls.chart_template = cls.env['account.chart.template']._load_records([{ + 'xml_id': 'account.test_chart_template', + 'values': { + 'name': 'Test Chart Template', + 'code_digits': 6, + 'currency_id': cls.env.ref('base.EUR').id, + 'bank_account_code_prefix': 1000, + 'cash_account_code_prefix': 2000, + 'transfer_account_code_prefix': 3000, + }, + }]) + + account_templates = cls.env['account.account.template']._load_records([ + { + 'xml_id': 'account.test_account_income_template', + 'values': + { + 'name': 'property_income_account', + 'code': '222221', + 'user_type_id': cls.env.ref('account.data_account_type_revenue').id, + 'chart_template_id': cls.chart_template.id, + } + }, + { + 'xml_id': 'account.test_account_expense_template', + 'values': + { + 'name': 'property_expense_account', + 'code': '222222', + 'user_type_id': cls.env.ref('account.data_account_type_expenses').id, + 'chart_template_id': cls.chart_template.id, + } + }, + ]) + + cls.chart_template.property_account_income_categ_id = account_templates[0].id + cls.chart_template.property_account_expense_categ_id = account_templates[1].id + + cls.tax_1_template = cls.create_tax_template('Tax 1', 'account.test_tax_1_template', 15) + cls.tax_2_template = cls.create_tax_template('Tax 2', 'account.test_tax_2_template', 0) + + cls.fiscal_position_template = cls.env['account.fiscal.position.template']._load_records([{ + 'xml_id': 'account.test_fiscal_position_template', + 'values': { + 'name': 'Fiscal Position', + 'chart_template_id': cls.chart_template.id, + 'country_id': cls.env.ref('base.be').id, + 'auto_apply': True, + }, + }]) + + cls.env['account.fiscal.position.tax.template']._load_records([{ + 'xml_id': 'account.test_fiscal_position_tax_template_1', + 'values': { + 'tax_src_id': cls.tax_1_template.id, + 'tax_dest_id': cls.tax_2_template.id, + 'position_id': cls.fiscal_position_template.id, + }, + }]) + + cls.chart_template.try_loading(company=cls.company_1, install_demo=False) + + cls.partner_1 = cls.env['res.partner'].create({ + 'name': 'Partner 1', + 'country_id': cls.env.ref('base.be').id, + }) + + def test_update_taxes_from_templates(self): + """ + Tests that adding a new tax template and a fiscal position tax template with this new tax template + creates this new tax and fiscal position line when updating + """ + fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_1.id) + tax_3_template = self.create_tax_template('Tax 3', 'account.test_tax_3_template', 16) + tax_4_template = self.create_tax_template('Tax 4', 'account.test_tax_4_template', 17) + + self.env['account.fiscal.position.tax.template']._load_records([ + { + 'xml_id': 'account.test_fiscal_position_new_tax_src_template', + 'values': { + 'tax_src_id': tax_3_template.id, + 'tax_dest_id': self.tax_1_template.id, + 'position_id': self.fiscal_position_template.id, + }, + }, + { + 'xml_id': 'account.test_fiscal_position_new_tax_dest_template', + 'values': { + 'tax_src_id': self.tax_2_template.id, + 'tax_dest_id': tax_4_template.id, + 'position_id': self.fiscal_position_template.id, + }, + }, + ]) + + taxes = self.env['account.tax'].search([('company_id', '=', self.company_1.id)]) + + # Only the template have been created, so it should not be reflected yet on the company's chart template + self.assertEqual(len(fiscal_position.tax_ids), 1) + self.assertEqual(len(taxes), 2) + + chart_template_xml_id = self.chart_template.get_external_id()[self.chart_template.id] + update_taxes_from_templates(self.env.cr, chart_template_xml_id) + + taxes = self.env['account.tax'].search([('company_id', '=', self.company_1.id)]) + self.assertRecordValues(taxes, [ + {'name': 'Tax 1'}, + {'name': 'Tax 2'}, + {'name': 'Tax 3'}, + {'name': 'Tax 4'}, + ]) + + self.assertRecordValues(fiscal_position.tax_ids.tax_src_id, [ + {'name': 'Tax 1'}, + {'name': 'Tax 3'}, + {'name': 'Tax 2'}, + ]) + self.assertRecordValues(fiscal_position.tax_ids.tax_dest_id, [ + {'name': 'Tax 2'}, + {'name': 'Tax 1'}, + {'name': 'Tax 4'}, + ]) + + def test_update_taxes_removed_from_templates(self): + """ + Tests updating after the removal of taxes and fiscal position mapping from the company + + """ + fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_1.id) + fiscal_position.tax_ids.unlink() + self.env['account.tax'].search([('company_id', '=', self.company_1.id)]).unlink() + + chart_template_xml_id = self.chart_template.get_external_id()[self.chart_template.id] + update_taxes_from_templates(self.env.cr, chart_template_xml_id) + + # if taxes have been deleted, they will be recreated, and the fiscal position mapping for it too + self.assertEqual(len(self.env['account.tax'].search([('company_id', '=', self.company_1.id)])), 2) + self.assertEqual(len(fiscal_position.tax_ids), 1) + + fiscal_position.tax_ids.unlink() + update_taxes_from_templates(self.env.cr, chart_template_xml_id) + + # if only the fiscal position mapping has been removed, it won't be recreated + self.assertEqual(len(fiscal_position.tax_ids), 0)