Odoo18-Base/addons/account/tests/test_chart_template.py
2025-01-06 10:57:38 +07:00

1035 lines
52 KiB
Python

import io
from markupsafe import Markup
from unittest.mock import patch
from odoo import Command
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.addons.account.models.chart_template import code_translations, AccountChartTemplate, TEMPLATE_MODELS
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
def _get_chart_template_mapping(self, get_all=False):
return {'test': {
'name': 'test',
'country_id': self.env.ref('base.be').id,
'country_code': None,
'module': 'account',
'parent': None,
}}
def test_get_data(self, template_code):
return {
'template_data': {
'code_digits': 6,
'currency_id': 'base.EUR',
'property_account_income_categ_id': 'test_account_income_template',
'property_account_expense_categ_id': 'test_account_expense_template',
'property_account_receivable_id': 'test_account_receivable_template',
'property_account_payable_id': 'test_account_payable_template',
},
'account.tax.group': {
'tax_group_taxes': {
'name': "Taxes",
'sequence': 0,
},
},
'account.journal': self._get_account_journal(template_code),
'res.company': {
self.env.company.id: {
'bank_account_code_prefix': '1000',
'cash_account_code_prefix': '2000',
'transfer_account_code_prefix': '3000',
'account_sale_tax_id': 'test_tax_1_template',
},
},
'account.account.tag': {
f'account.account_tax_tag_{i}': {
'name': f'tax_tag_name_{i}',
'applicability': 'taxes',
'country_id': 'base.be',
} for i in range(1, 9)
},
'account.tax': {
xmlid: _tax_vals(name, amount, 'account.account_tax_tag_1')
for name, xmlid, amount in [
('Tax 1', 'test_tax_1_template', 15),
('Tax 2', 'test_tax_2_template', 0),
]
},
'account.group': {
'test_account_group_1': {
'name': 'test_account_group_name_1',
'code_prefix_start': 222220,
'code_prefix_end': 222229,
}
},
'account.account': {
'test_account_receivable_template': {
'name': 'property_receivable_account',
'code': '411111',
'account_type': 'asset_receivable',
},
'test_account_payable_template': {
'name': 'property_payable_account',
'code': '421111',
'account_type': 'liability_payable',
},
'test_account_income_template': {
'name': 'property_income_account',
'code': '222221',
'account_type': 'income',
'group_id': 'test_account_group_1',
},
'test_account_expense_template': {
'name': 'property_expense_account',
'code': '222222',
'account_type': 'expense',
},
},
'account.fiscal.position': {
'test_fiscal_position_template': {
'name': 'Fiscal Position',
'country_id': 'base.be',
'auto_apply': True,
'tax_ids': [
Command.create({
'tax_src_id': 'test_tax_1_template',
'tax_dest_id': 'test_tax_2_template',
})
]
}
},
'account.reconcile.model': {
'test_account_reconcile_model_1': {
'name': 'test_reconcile_model_with_payment_tolerance',
'rule_type': 'invoice_matching',
'allow_payment_tolerance': True,
'payment_tolerance_type': 'percentage',
'payment_tolerance_param': 2.0,
'line_ids': [Command.create({'account_id': 'test_account_income_template'})],
}
}
}
def _tax_vals(name, amount, tax_tag_id=None, children_tax_xmlids=None, active=True, tax_scope="consu"):
tag_command = [Command.set([tax_tag_id])] if tax_tag_id else None
tax_vals = {
'name': name,
'amount': amount,
'amount_type': 'percent' if not children_tax_xmlids else 'group',
'tax_group_id': 'tax_group_taxes',
'active': active,
'tax_scope': tax_scope
}
if children_tax_xmlids:
tax_vals.update({'children_tax_ids': [Command.set(children_tax_xmlids)]})
else:
tax_vals.update({'repartition_line_ids': [
Command.create({'document_type': 'invoice', 'factor_percent': 100, 'repartition_type': 'base', 'tag_ids': tag_command}),
Command.create({'document_type': 'invoice', 'factor_percent': 100, 'repartition_type': 'tax'}),
Command.create({'document_type': 'refund', 'factor_percent': 100, 'repartition_type': 'base'}),
Command.create({'document_type': 'refund', 'factor_percent': 100, 'repartition_type': 'tax'}),
]})
return tax_vals
CSV_DATA = {
'tax_1': (
'"id","name","type_tax_use","amount","amount_type","description","invoice_label","tax_group_id","repartition_line_ids/repartition_type",'
'"repartition_line_ids/factor_percent","repartition_line_ids/document_type","repartition_line_ids/tag_ids","repartition_line_ids/account_id",'
'"repartition_line_ids/use_in_tax_closing","description@en"\n'
'"tax_1","5%","sale","5.0","percent","","VAT 5%","tax_group_taxes","base","","invoice","tax_tag_name_1||tax_tag_name_2","","","Test tax"\n'
'"","","","","","","","","tax","50","invoice","tax_tag_name_3","test_account_income_template","False",""\n'
'"","","","","","","","","tax","50","invoice","tax_tag_name_4","test_account_income_template","False",""\n'
'"","","","","","","","","base","","refund","tax_tag_name_5||tax_tag_name_6","","",""\n'
'"","","","","","","","","tax","50","refund","tax_tag_name_7","test_account_income_template","False",""\n'
'"","","","","","","","","tax","50","refund","tax_tag_name_8","test_account_income_template","False",""\n'
),
'test_fiscal_position_template': (
'"id","name","country_id","auto_apply","tax_ids/tax_src_id","tax_ids/tax_dest_id"\n'
'"test_fiscal_position_template","Fiscal Position","base.be","1","test_tax_3_template","test_tax_4_template"\n'
),
}
@tagged('post_install', '-at_install')
@patch.object(AccountChartTemplate, '_get_chart_template_mapping', _get_chart_template_mapping)
class TestChartTemplate(AccountTestInvoicingCommon):
@classmethod
def _use_chart_template(cls, company, chart_template_ref=None):
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
cls.env['account.chart.template'].try_loading("test", company=company, install_demo=False)
@classmethod
@AccountTestInvoicingCommon.setup_country('be')
@patch.object(AccountChartTemplate, '_get_chart_template_mapping', _get_chart_template_mapping)
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
"""
# Avoid creating data from AccountTestInvoicingCommon setUpClass
# just use the override of the functions it provides
super(AccountTestInvoicingCommon, cls).setUpClass()
cls.ChartTemplate = cls.env['account.chart.template'].with_company(cls.company)
cls.country_be = cls.env.ref('base.be')
def test_signed_and_unsigned_tags_tax(self):
tax_report = self.env['account.report'].create({
'name': "Tax report 1",
'country_id': self.country_be.id,
'column_ids': [
Command.create({
'name': "Balance",
'expression_label': 'balance',
}),
],
})
self.env['account.report.line'].create({
'name': "[SIGNED_TAG] Signed tag line",
'report_id': tax_report.id,
'sequence': max(tax_report.mapped('line_ids.sequence') or [0]) + 1,
'expression_ids': [
Command.create({
'label': 'balance',
'engine': 'tax_tags',
'formula': 'SIGNED_TAG',
}),
],
})
signed_tag = self.env['account.account.tag'].search([
('applicability', '=', 'taxes'),
('name', '=', '+SIGNED_TAG'),
])
self.env['account.account.tag']._load_records([
{
'xml_id': 'account.unsigned_tax_tag',
'noupdate': True,
'values': {
'name': "unsigned tax tag",
'applicability': 'taxes',
'country_id': self.country_be.id,
},
},
])
tax_to_load = {
'name': 'Mixed Tags Tax',
'amount': 30,
'amount_type': 'percent',
'tax_group_id': 'tax_group_taxes',
'active': True,
'repartition_line_ids': [
Command.create({'document_type': 'invoice', 'factor_percent': 100, 'repartition_type': 'base', 'tag_ids': 'account.unsigned_tax_tag||+SIGNED_TAG'}),
Command.create({'document_type': 'invoice', 'factor_percent': 100, 'repartition_type': 'tax'}),
Command.create({'document_type': 'refund', 'factor_percent': 100, 'repartition_type': 'base'}),
Command.create({'document_type': 'refund', 'factor_percent': 100, 'repartition_type': 'tax'}),
]
}
self.env['account.chart.template']._deref_account_tags('test', {'tax1': tax_to_load})
self.assertEqual(
tax_to_load['repartition_line_ids'][0],
Command.create({
'document_type': 'invoice',
'factor_percent': 100,
'repartition_type': 'base',
'tag_ids': [Command.set(['account.unsigned_tax_tag', signed_tag.id])],
})
)
def test_inactive_tag_tax(self):
inactive_tag = self.env['account.account.tag'].create({
'name': 'Inactive Tax Tag',
'applicability': 'taxes',
'active': False,
'country_id': self.country_be.id,
})
tax_to_load = {
'name': 'Inactive Tags Tax',
'amount': 30,
'amount_type': 'percent',
'tax_group_id': 'tax_group_taxes',
'active': True,
'repartition_line_ids': [
Command.create({
'document_type': 'invoice',
'factor_percent': 100,
'repartition_type': 'base',
'tag_ids': inactive_tag.name,
}),
]
}
self.env['account.chart.template']._deref_account_tags('test', {'tax1': tax_to_load})
self.assertEqual(
tax_to_load['repartition_line_ids'][0],
Command.create({
'document_type': 'invoice',
'factor_percent': 100,
'repartition_type': 'base',
'tag_ids': [(Command.set([inactive_tag.id]))],
}),
)
def test_update_taxes_creation(self):
""" Tests that adding a new tax and a fiscal position tax creates new records when updating. """
def local_get_data(self, template_code):
data = test_get_data(self, template_code)
data['account.tax'].update({
xmlid: _tax_vals(name, amount)
for name, xmlid, amount in [
('Tax 3', 'test_tax_3_template', 16),
('Tax 4', 'test_tax_4_template', 17),
]
})
data['account.fiscal.position']['test_fiscal_position_template']['tax_ids'].extend([
Command.create({
'tax_src_id': 'test_tax_3_template',
'tax_dest_id': 'test_tax_1_template',
}),
Command.create({
'tax_src_id': 'test_tax_2_template',
'tax_dest_id': 'test_tax_4_template',
}),
])
return data
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
taxes = self.env['account.tax'].search([('company_id', '=', self.company.id)])
self.assertRecordValues(taxes, [
{'name': 'Tax 1'},
{'name': 'Tax 2'},
{'name': 'Tax 3'},
{'name': 'Tax 4'},
])
fiscal_position = self.env['account.fiscal.position'].search([])
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_new_tax_rate(self):
"""Test the flow to replace taxes from an old to a new rate.
This test aims at defining more clearly how to update the taxes when the rate changes.
There are some constraints to take into account:
* There is a transition period where both taxes need to be usable
* Some reports might need to keep the references to the old tax i.e. on the fiscal positions (OSS)
The procedure for the code change is:
* Create the new report section if needed.
* Remove the taxes from the template.
This will not delete the taxes from the company when updating the template on the company.
* Add the new taxes with a xmlid different from the ones deleted.
The taxes will be created when updating the template on the company.
* Update all the references to the old taxes to the new ones.
- Fiscal positions: The previous mappings won't be deleted but the new ones will be created.
This ensures that reports still work.
- Company: The default sales/purchase taxes should be updated. It should only impact the creation
of new products so it is probably not going to be an issue.
* If such a tax was part of a group, a new group must be created and the old one removed from the template.
The procedure for the users, when they are ready, is:
* When the old taxes are not needed anymore (i.e. tax period closed), archive them
* Update the taxes on
- products
- accounts
- reconcile models
"""
def local_get_data(self, template_code):
# Delete the existing tax and create a new one with a different rate
data = test_get_data(self, template_code)
del data['account.tax']['test_tax_1_template']
data['account.tax']['test_tax_3_template'] = _tax_vals('Tax 3', 30)
for fpos in data['account.fiscal.position'].values():
for _command, _id, tax in fpos['tax_ids']:
if tax['tax_src_id'] == 'test_tax_1_template':
tax['tax_src_id'] = 'test_tax_3_template'
if tax['tax_dest_id'] == 'test_tax_1_template':
tax['tax_dest_id'] = 'test_tax_3_template'
data['res.company'][self.env.company.id]['account_sale_tax_id'] = 'test_tax_3_template'
return data
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
# On an existing company all the taxes are still present.
# The user has to deactivate them manually when not needed anymore (if they needed to encode things in the past)
taxes = self.env['account.tax'].search([('company_id', '=', self.company.id)])
self.assertRecordValues(taxes, [
{'name': 'Tax 1'},
{'name': 'Tax 2'},
{'name': 'Tax 3'},
])
tax_1, tax_2, tax_3 = taxes
fiscal_position = self.env['account.fiscal.position'].search([('company_id', '=', self.company.id)])
self.assertRecordValues(fiscal_position.tax_ids, [
{'tax_src_id': tax_1.id, 'tax_dest_id': tax_2.id},
{'tax_src_id': tax_3.id, 'tax_dest_id': tax_2.id},
])
self.assertEqual(self.company.account_sale_tax_id, tax_3)
# On a new company you would never see the old tax.
# In case users need it, they can duplicate the new one and change the rate.
new_company = self.env['res.company'].create({'name': 'New Company'})
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=new_company, install_demo=False)
taxes = self.env['account.tax'].search([('company_id', '=', new_company.id)])
self.assertRecordValues(taxes, [
{'name': 'Tax 2'},
{'name': 'Tax 3'},
])
tax_2, tax_3 = taxes
fiscal_position = self.env['account.fiscal.position'].search([('company_id', '=', new_company.id)])
self.assertRecordValues(fiscal_position.tax_ids, [
{'tax_src_id': tax_3.id, 'tax_dest_id': tax_2.id},
])
self.assertEqual(new_company.account_sale_tax_id, tax_3)
def test_update_taxes_update(self):
""" When a tax is close enough from an existing tax we want to update that tax with the new values. """
def local_get_data(self, template_code):
data = test_get_data(self, template_code)
data['account.account.tag']['account.account_tax_tag_1']['name'] += ' [DUP]'
return data
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
updated_tax = self.env['account.tax'].search([('company_id', '=', self.company.id), ('name', 'like', '%Tax 1')])
# Check that tax was not recreated
self.assertEqual(len(updated_tax), 1)
# Check that tags have been updated
self.assertEqual(updated_tax.invoice_repartition_line_ids.tag_ids.name, 'tax_tag_name_1 [DUP]')
def test_update_taxes_update_rounding(self):
"""
When a tax is close enough to an existing tax but has a minor rounding error,
we still want to update that tax with the new values.
"""
def local_get_data(self, template_code):
data = test_get_data(self, template_code)
data['account.account.tag']['account.account_tax_tag_1']['name'] += ' [DUP]'
# We compare up to the precision of the field, which is 4 decimals
data['account.tax']['test_tax_1_template']['amount'] += 0.00001
return data
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
updated_tax = self.env['account.tax'].search([('company_id', '=', self.company.id), ('name', 'like', '%Tax 1')])
# Check that tax was not recreated
self.assertEqual(len(updated_tax), 1)
# Check that tags have been updated
self.assertEqual(updated_tax.invoice_repartition_line_ids.tag_ids.name, 'tax_tag_name_1 [DUP]')
def test_update_taxes_recreation(self):
""" When a tax is too different from an existing tax we want to recreate a new tax with new values. """
def local_get_data(self, template_code):
# We increment the amount so the template gets slightly different from the
# corresponding tax and triggers recreation
data = test_get_data(self, template_code)
data['account.tax']['test_tax_1_template']['name'] = 'Tax 1 modified'
data['account.tax']['test_tax_1_template']['amount'] += 1
return data
tax_existing = self.env['account.tax'].search([('company_id', '=', self.company.id), ('name', '=', 'Tax 1')])
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
# Check that old tax has not been changed beside of the name prefixed by [old]
self.assertRecordValues(tax_existing, [{'name': '[old] Tax 1', 'amount': 15}])
# Check that new tax has been recreated
new_tax = self.env['account.tax'].search([('company_id', '=', self.company.id), ('name', '=', 'Tax 1 modified')])
self.assertEqual(new_tax.amount, tax_existing.amount + 1)
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'].search([])
fiscal_position.tax_ids.unlink()
self.env['account.tax'].search([('company_id', '=', self.company.id)]).unlink()
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
# 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.id)])), 2)
self.assertEqual(len(fiscal_position.tax_ids), 1)
fiscal_position.tax_ids.unlink()
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
# if only the fiscal position mapping has been removed, it won't be recreated
self.assertEqual(len(fiscal_position.tax_ids), 0)
def test_update_taxes_conflict_name(self):
def local_get_data(self, template_code):
data = test_get_data(self, template_code)
data['account.tax']['test_tax_1_template']['amount'] = 40
return data
def local_get_data2(self, template_code):
data = test_get_data(self, template_code)
data['account.tax']['test_tax_1_template']['amount'] = 15
return data
tax_1_existing = self.env['account.tax'].search([('company_id', '=', self.company.id), ('name', '=', "Tax 1")])
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
tax_1_old = self.env['account.tax'].search([('company_id', '=', self.company.id), ('name', '=', "[old] Tax 1")])
tax_1_new = self.env['account.tax'].search([('company_id', '=', self.company.id), ('name', '=', "Tax 1")])
self.assertEqual(tax_1_old, tax_1_existing, "Old tax still exists but with a different name.")
self.assertEqual(len(tax_1_new), 1, "New tax have been created with the original name.")
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data2, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
tax_1_old_first = self.env['account.tax'].search([('company_id', '=', self.company.id), ('name', '=', "[old] Tax 1")])
tax_1_old_second = self.env['account.tax'].search([('company_id', '=', self.company.id), ('name', '=', "[old1] Tax 1")])
tax_1_latest = self.env['account.tax'].search([('company_id', '=', self.company.id), ('name', '=', "Tax 1")])
self.assertEqual(tax_1_old, tax_1_old_first, "Old renamed tax is still the same.")
self.assertEqual(tax_1_old_second, tax_1_new, "Outdated tax is renamed again.")
self.assertEqual(len(tax_1_latest), 1, "New tax have been created with the original name.")
def test_update_taxes_multi_company(self):
""" In a multi-company environment all companies should be correctly updated."""
def local_get_data(self, template_code):
# triggers recreation of tax 1
data = test_get_data(self, template_code)
data['account.tax']['test_tax_1_template']['amount'] += 1
return data
company_2 = self.env['res.company'].create({
'name': 'TestCompany2',
'country_id': self.env.ref('base.be').id,
})
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=company_2, install_demo=False)
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
self.env['account.chart.template'].try_loading('test', company=company_2, install_demo=False)
taxes_1_companies = self.env['account.tax'].search([
('name', '=like', '%Tax 1'),
('company_id', 'in', [self.company.id, company_2.id]),
])
# we should have 4 records: 2 companies * (1 original tax + 1 recreated tax)
self.assertEqual(len(taxes_1_companies), 4)
def test_update_account_codes_conflict(self):
# Change code of an existing account to something else
standard_account = self.env['account.chart.template'].ref('test_account_income_template')
standard_account.code = '111111'
# create a new account with the same code as an existing one
problematic_account = self.env['account.account'].create({
'code': '222221',
'name': 'problematic_account',
})
# remove an xmlid to see if it gets relinked and not duplicated
self.env['ir.model.data'].search([
('name', '=', f'{self.company.id}_test_account_expense_template'),
('module', '=', 'account'),
]).unlink()
# reload chart template
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
# check that xmlid is now pointing to problematic_account
xmlid_account = self.env.ref(f'account.{self.company.id}_test_account_income_template')
self.assertEqual(problematic_account, xmlid_account, "xmlid is not pointing to the right account")
def test_update_taxes_children_tax_ids(self):
""" Ensures children_tax_ids are correctly generated when updating taxes with
amount_type='group'.
"""
def local_get_data(self, template_code):
data = test_get_data(self, template_code)
normal_tax_xmlids = ['test_tax_3_template', 'test_tax_4_template']
data['account.tax'].update({
xmlid: _tax_vals(name, amount, children_tax_xmlids=children_tax_xmlids)
for name, xmlid, amount, children_tax_xmlids in [
('Tax 3', normal_tax_xmlids[0], 16, None),
('Tax 4', normal_tax_xmlids[1], 17, None),
('Tax with children', 'test_tax_5_group_template', 0, normal_tax_xmlids)
]
})
return data
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
parent_tax = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', '=', 'Tax with children'),
])
children_taxes = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', 'in', ['Tax 3', 'Tax 4']),
])
self.assertEqual(len(parent_tax), 1, "The parent tax should have been created.")
self.assertEqual(len(children_taxes), 2, "Two children should have been created.")
self.assertEqual(parent_tax.children_tax_ids.ids, children_taxes.ids, "The parent and its children taxes should be linked together.")
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
# We don't change anything
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
self.assertEqual(parent_tax.name, 'Tax with children', "The parent tax created before should not have changed")
def test_update_taxes_children_tax_ids_inactive(self):
""" Ensure tax templates are correctly generated when updating taxes with children taxes,
even if templates are inactive.
"""
def local_get_data(self, template_code):
data = test_get_data(self, template_code)
normal_tax_xmlids = ['test_tax_3_template', 'test_tax_4_template']
data['account.tax'].update({
xmlid: _tax_vals(name, amount, children_tax_xmlids=children_tax_xmlids, active=active)
for name, xmlid, amount, children_tax_xmlids, active in [
('Inactive Tax 3', normal_tax_xmlids[0], 16, None, False),
('Inactive Tax 4', normal_tax_xmlids[1], 17, None, False),
('Inactive Tax with children', 'test_tax_5_group_template', 0, normal_tax_xmlids, False)
]
})
return data
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
parent_tax = self.env['account.tax'].with_context(active_test=False).search([
('company_id', '=', self.company.id),
('name', '=', 'Inactive Tax with children'),
])
children_taxes = self.env['account.tax'].with_context(active_test=False).search([
('company_id', '=', self.company.id),
('name', 'in', ['Inactive Tax 3', 'Inactive Tax 4']),
])
self.assertEqual(len(parent_tax), 1, "The parent tax should have been created, even if it is inactive.")
self.assertFalse(parent_tax.active, "The parent tax should be inactive.")
self.assertEqual(len(children_taxes), 2, "Two children should have been created, even if they are inactive.")
self.assertEqual(children_taxes.mapped('active'), [False] * 2, "Children taxes should be inactive.")
def test_update_reload_no_new_data(self):
""" Tests that the reload does nothing when data are left unchanged.
Tested models: account.group, account.account, account.tax.group, account.tax, account.journal,
account.reconcile.model, account.fiscal.position, account.fiscal.position.tax, account.tax.repartition.line,
account.account.tag.
"""
def get_domain(model):
if model == 'account.account.tag':
return [('country_id', '=', self.company.country_id.id)]
elif model == 'account.account':
return [('company_ids', '=', self.company.id)]
else:
return [('company_id', '=', self.company.id)]
sub_models = ('account.fiscal.position.tax', 'account.tax.repartition.line', 'account.account.tag')
data_before = {}
for model in TEMPLATE_MODELS + sub_models:
data_before[model] = self.env[model].search(get_domain(model))
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
for model in TEMPLATE_MODELS + sub_models:
data_after = self.env[model].search(get_domain(model))
self.assertEqual(data_before[model], data_after)
def test_unknown_company_fields(self):
""" Tests that if a key is not known in the company template data when the
context value 'l10n_check_fields_complete' is set, an error is raised. If a
key is not known in the company template data but the context value is not
set, that key is skipped and no error is raised."""
def local_get_data(self, template_code):
data = test_get_data(self, template_code)
data['res.company'][company.id]['unknown_company_key'] = 'unknown_company_value'
return data
company = self.company
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
# hard fail the loading if the context key is set to ensure `test_all_l10n` works as expected
with (
self.assertRaisesRegex(ValueError, 'unknown_company_key'),
self.env.cr.savepoint(),
):
self.env['account.chart.template'].with_context(l10n_check_fields_complete=True).try_loading('test', company=company, install_demo=False)
# silently ignore if the field doesn't exist (yet)
self.env['account.chart.template'].try_loading('test', company=company, install_demo=False)
def test_branch(self):
# Test the auto-installation of a chart template (including demo data) on a branch
# Create a new main company, because install_demo doesn't do anything when reloading data
company = self.env['res.company'].create([{'name': 'Test Company'}])
branch = self.env['res.company'].create([{
'name': 'Test Branch',
'parent_id': company.id,
}])
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=company, install_demo=True)
self.assertEqual(company.chart_template, 'test')
self.assertEqual(branch.chart_template, 'test')
def test_change_coa(self):
def _get_chart_template_mapping(self, get_all=False):
return {'other_test': {
'name': 'test',
'country_id': None,
'country_code': None,
'module': 'account',
'parent': None,
}}
# Check that company fields that should depend on CoA are reset when changing CoA
# (afaik there is only `anglo_saxon_accounting`)
self.company.anglo_saxon_accounting = True
with (
patch.object(AccountChartTemplate, '_get_chart_template_mapping', _get_chart_template_mapping),
patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True)
):
self.env['account.chart.template'].try_loading('other_test', company=self.company, install_demo=True)
# Create a branch and an unrelated company
branch, other_company = self.env['res.company'].create([
{
'name': 'Test Branch',
'parent_id': self.company.id,
},
{
'name': 'Other Test Company',
},
])
# Run precommit hook to load the template on the branch
self.env.cr.precommit.run()
self.assertEqual(self.company.chart_template, 'other_test')
self.assertEqual(branch.chart_template, 'other_test')
self.assertFalse(self.company.anglo_saxon_accounting)
# Setup a shared account, belonging to the company, the branch, and the unrelated company
shared_account = self.env['account.account'].create([{
'name': 'Shared Account',
'company_ids': [Command.set((self.company | branch | other_company).ids)],
'code_mapping_ids': [
Command.create({'company_id': self.company.id, 'code': '180001'}),
Command.create({'company_id': branch.id, 'code': '180001'}),
Command.create({'company_id': other_company.id, 'code': '180001'}),
],
}])
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=True)
self.assertEqual(self.company.chart_template, 'test')
self.assertEqual(branch.chart_template, 'test')
# Check that the shared account was not deleted, but just unlinked from the company and the branch.
self.assertEqual(shared_account.company_ids, other_company)
def test_update_tax_with_non_existent_tag(self):
""" Tests that when we update the CoA with a tax that has a tag that does not exist yet we raise an error.
Typical use case is when the code got updated but the module haven't been updated (-u).
"""
tax_to_load = {
'name': 'Mixed Tags Tax',
'amount': 30,
'amount_type': 'percent',
'tax_group_id': 'tax_group_taxes',
'active': True,
'repartition_line_ids': [
Command.create({'document_type': 'invoice', 'factor_percent': 100, 'repartition_type': 'base', 'tag_ids': '+SIGNED_TAG'}),
Command.create({'document_type': 'invoice', 'factor_percent': 100, 'repartition_type': 'tax'}),
Command.create({'document_type': 'refund', 'factor_percent': 100, 'repartition_type': 'base'}),
Command.create({'document_type': 'refund', 'factor_percent': 100, 'repartition_type': 'tax'}),
]
}
with self.assertRaisesRegex(UserError, 'update your localization'):
self.env['account.chart.template']._deref_account_tags('test', {'tax1': tax_to_load})
def test_install_with_translations(self):
""" Ensure that the translations are loaded correctly when installing chart data; i.e. test '_load_translations' and that the untranslatable fields are translated correctly.
Note: The '_load_translations' function depends on the '_get_chart_template_data' function for some information.
The result of '_get_chart_template_data' is mocked (correctly) in this test (and not tested).
"""
# Local mock for '_get_chart_template_mapping'
# We will use / install a dedicated new chart 'translation' (not just reload 'test')
# To have control over the original / en_US values.
def local_get_mapping(self, get_all=False):
return {'translation': {
'name': 'translation',
'country_id': None,
'country_code': None,
'modules': ['account'],
'parent': None,
}}
company = self.company
# Create records that are not part of the chart template
# They will be translated via code translations.
# The module used to source the translation is the module from the xml_id or 'account' (as fallback)
non_chart_data = {
'account.group': {
# try module 'no_translation'; fallback to 'account'
'no_translation.test_chart_template_company_test_free_account_group': {
'name': 'Free Account Group',
'code_prefix_start': 333330,
'code_prefix_end': 333339,
'company_id': company.id,
},
},
'account.account': {
# translate via 'translation' module
'translation.test_chart_template_company_test_free_account': {
'name': 'Free Account',
'code': '333331',
'account_type': 'asset_current',
'company_ids': [Command.link(company.id)],
},
},
'account.tax': {
# translate via 'translation' module;
# 2 translatable fields ('name' and 'description')
'translation.test_chart_template_company_test_free_tax': {
"name": "Free Tax",
"description": "Free Tax Description",
"amount": "0.00",
"company_id": company.id,
},
},
}
# Local function to "extend" '_post_load_data' to ensure the creation of the records from 'non_chart_data'
def test_post_load_data(template_code, company, template_data):
for model, data in non_chart_data.items():
for xml_id, values in data.items():
self.env[model]._load_records([{
'xml_id': xml_id,
'values': values,
}])
# Create a local mock of '_get_chart_template_data'; "extend" 'test_get_data' with the translation info
translation_update_for_test_get_data = {
# Use code translations from module 'translation'
'account.journal': {
'cash': {
'name': "Cash",
'code': "C", # untranslatable field; shortened due to length restriction (for _translation)
'__translation_module__': {
'name': 'translation',
'code': 'translation',
},
},
},
# Different modules for code translations of 'name' and 'description'
'account.tax': {
'test_tax_1_template': {
'name': "Tax 1",
'description': "Tax 1 Description",
'__translation_module__': {
'name': 'translation',
'description': 'translation2',
},
},
},
# Use 'name@' and not code translation
'account.tax.group': {
'tax_group_taxes': {
'name': "Taxes",
'name@fr': "Taxes FR",
'__translation_module__': {
'name': 'translation',
},
},
},
}
def local_get_data(self, template_code):
data = test_get_data(self, template_code)
for model, record_info in translation_update_for_test_get_data.items():
for xmlid, data_update in record_info.items():
data[model][xmlid].update(data_update)
return data
# Tranlations should fall back to more generic locale 'fr'
# Target lang for untranslatable fields
company.partner_id.lang = self.env['res.lang']._activate_lang('fr_BE').code
# Init empty mock translations to make sure we do not use unintended translation
mock_python_translations = {}
for module, lang, value, translation in [
# wrong translations
('translation', 'fr', "Taxes", "WRONG"), # there is a value in the chart data
('translation', 'fr', "Free Account", "Free Account FR"), # there is a value for fr_BE
# correct translations
('translation', 'fr', "Cash", "Cash FR"),
('translation', 'fr', "C", "C FR"),
('translation', 'fr', "Tax 1", "Tax 1 FR"),
('translation', 'fr_BE', "Free Account", "Free Account FR_BE"),
('translation', 'fr', "Free Tax", "Free Tax FR"),
('translation', 'fr', "Free Tax Description", "Free Tax Description FR"),
('translation2', 'fr', "Tax 1 Description", "Tax 1 Description translation2/FR"),
('account', 'fr', "Free Account Group", "Free Account Group account/FR"),
]:
mock_python_translations.setdefault((module, lang), {})[value] = translation
with patch.object(AccountChartTemplate, '_get_chart_template_mapping', side_effect=local_get_mapping, autospec=True):
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
with patch.object(AccountChartTemplate, '_post_load_data', wraps=test_post_load_data):
with patch.object(code_translations, 'python_translations', mock_python_translations):
self.env['account.chart.template'].try_loading('translation', company=company, install_demo=False)
# Check translations
translatable_model_fields = self.env['account.chart.template']._get_translatable_template_model_fields()
untranslatable_model_fields = self.env['account.chart.template']._get_untranslatable_fields_to_translate()
fields_to_translate = {
model: set(translatable_model_fields.get(model, []) + untranslatable_model_fields.get(model, []))
for model in TEMPLATE_MODELS
}
self.assertEqual({
f'{xmlid}.{field}@{lang}': self.env['account.chart.template'].ref(xmlid).with_context(lang=lang)[field]
for chart_like_data in [non_chart_data, translation_update_for_test_get_data]
for model, data in chart_like_data.items()
for xmlid, record_data in data.items()
for field in record_data if field in fields_to_translate.get(model, set())
for lang in ['en_US', 'fr_BE']
}, {
'cash.code@en_US': 'C FR', # untranslatable field loaded in lang fr_BE
'cash.code@fr_BE': 'C FR',
'cash.name@en_US': 'Cash',
'cash.name@fr_BE': 'Cash FR',
'no_translation.test_chart_template_company_test_free_account_group.name@en_US': 'Free Account Group',
'no_translation.test_chart_template_company_test_free_account_group.name@fr_BE': 'Free Account Group account/FR', # fallback to account
'tax_group_taxes.name@en_US': 'Taxes',
'tax_group_taxes.name@fr_BE': 'Taxes FR',
'test_tax_1_template.description@en_US': Markup('<p>Tax 1 Description</p>'),
'test_tax_1_template.description@fr_BE': Markup('Tax 1 Description translation2/FR'),
'test_tax_1_template.name@en_US': 'Tax 1',
'test_tax_1_template.name@fr_BE': 'Tax 1 FR',
'translation.test_chart_template_company_test_free_account.name@en_US': 'Free Account',
'translation.test_chart_template_company_test_free_account.name@fr_BE': 'Free Account FR_BE', # do not use generic lang
'translation.test_chart_template_company_test_free_tax.description@en_US': Markup('<p>Free Tax Description</p>'),
'translation.test_chart_template_company_test_free_tax.description@fr_BE': Markup('<p>Free Tax Description</p>'),
'translation.test_chart_template_company_test_free_tax.name@en_US': 'Free Tax',
'translation.test_chart_template_company_test_free_tax.name@fr_BE': 'Free Tax FR',
})
def test_parsed_csv_submodel_being_loaded(self):
def get_rep_line_data(x):
return (x.document_type, x.repartition_type, x.factor_percent, x.use_in_tax_closing)
with patch('odoo.addons.account.models.chart_template.file_open',
side_effect=lambda *args: io.StringIO(CSV_DATA['tax_1'])):
data = {'account.tax': self.ChartTemplate._get_account_tax('test')}
self.ChartTemplate._load_data(data)
tax_1 = self.env.ref(f'account.{self.company.id}_tax_1', raise_if_not_found=False)
tax_rep_lines = tax_1.repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
self.assertEqual([
('invoice', 'tax', 50.0, False),
('invoice', 'tax', 50.0, False),
('refund', 'tax', 50.0, False),
('refund', 'tax', 50.0, False),
], tax_rep_lines.mapped(get_rep_line_data))
def test_parsed_csv_submodel_being_updated(self):
def local_get_data(self, template_code):
return {
**test_get_data(self, template_code),
'account.tax': {
xmlid: _tax_vals(name, amount)
for name, xmlid, amount in [
('Tax 1', 'test_tax_1_template', 15),
('Tax 2', 'test_tax_2_template', 0),
('Tax 3', 'test_tax_3_template', 16),
('Tax 4', 'test_tax_4_template', 17),
]
},
}
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
with patch('odoo.addons.account.models.chart_template.file_open',
side_effect=lambda *args: io.StringIO(CSV_DATA['test_fiscal_position_template'])):
data = {'account.fiscal.position': self.ChartTemplate._get_account_fiscal_position('test')}
self.ChartTemplate._pre_reload_data(self.company, {}, data)
self.ChartTemplate._load_data(data)
def test_command_int_values(self):
""" Command int values should just work in place of their Enum alternatives. """
def local_get_data(self, template_code):
data = test_get_data(self, template_code)
data['account.account'].update({
"test_account": {
'name': "Test account A",
'code': '777777',
'account_type': 'income_other',
'tag_ids': [(6, 0, self.ref('account.account_tag_investing').ids)],
},
"test_account_2": {
'name': "Test account B",
'code': '777778',
'account_type': 'income_other',
'tag_ids': [
(5, 0, 0),
(0, 0, {'name': 'Test account tag', 'applicability': 'accounts'}),
(0, 0, {'name': 'Test account tag 2', 'applicability': 'accounts'}),
]}
})
return data
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=local_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=self.company, install_demo=False)
accounts = self.env['account.account'].search([
('company_ids', '=', self.company.id),
('code', 'in', ('777777', '777778'))
], order='code asc')
self.assertEqual(2, len(accounts))
self.assertEqual(self.env.ref('account.account_tag_investing'), accounts[0].tag_ids)
self.assertEqual({'Test account tag', 'Test account tag 2'}, set(accounts[1].tag_ids.mapped("name")))
def test_chart_template_company_without_country(self):
"""
In this test we will try to install a chart template to a company without a country. The expected behavior
is that the country of the chart template will be set on the company
"""
company = self.env['res.company'].create({'name': 'Test Company Without country'})
self.assertFalse(company.country_id)
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
self.env['account.chart.template'].try_loading('test', company=company, install_demo=False)
self.assertEqual(company.country_id.code, "BE")