diff --git a/addons/l10n_eu_oss/models/__init__.py b/addons/l10n_eu_oss/models/__init__.py index 2eae557877a..8e08b574309 100644 --- a/addons/l10n_eu_oss/models/__init__.py +++ b/addons/l10n_eu_oss/models/__init__.py @@ -3,5 +3,6 @@ from . import chart_template from . import eu_tax_map +from . import eu_tag_map from . import res_company from . import res_config_settings diff --git a/addons/l10n_eu_oss/models/eu_tag_map.py b/addons/l10n_eu_oss/models/eu_tag_map.py new file mode 100644 index 00000000000..6c3da4c1384 --- /dev/null +++ b/addons/l10n_eu_oss/models/eu_tag_map.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +""" +The EU_TAG_MAP answers the question: "which tag should I apply on the OSS tax repartition line?" + +{ + 'fiscal_country_code': { + 'invoice_base_tag': xml_id_of_the_tag or None, + 'invoice_tax_tag': xml_id_of_the_tag or None, + 'refund_base_tag': xml_id_of_the_tag or None, + 'refund_tax_tag': xml_id_of_the_tag or None, + }, +} +""" + +EU_TAG_MAP = { + # Austria + 'l10n_at.l10n_at_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Belgium + 'l10n_be.l10nbe_chart_template': { + 'invoice_base_tag': 'l10n_be.tax_report_line_47', + 'invoice_tax_tag': None, + 'refund_base_tag': 'l10n_be.tax_report_line_49', + 'refund_tax_tag': None, + }, + # Bulgaria + 'BG': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Croatia + 'l10n_hr.l10n_hr_chart_template_rrif': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Cyprus + 'CY': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Czech - Done in 13.0 - CoA not available yet + 'l10n_cz.cz_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Denmark + 'l10n_dk.dk_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Estonia - Done in 13.0 - CoA not available yet + 'EE': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Finland + 'l10n_fi.fi_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # France + 'l10n_fr.l10n_fr_pcg_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Germany SKR03 + 'l10n_de_skr03.l10n_de_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Germany SKR04 + 'l10n_de_skr04.l10n_chart_de_skr04': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Greece + 'l10n_gr.l10n_gr_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Hungary + 'l10n_hu.hungarian_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Ireland + 'l10n_ie.l10n_ie': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Italy + 'l10n_it.l10n_it_chart_template_generic': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Latvia + 'LV': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Lithuania + 'l10n_lt.account_chart_template_lithuania': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Luxembourg + 'l10n_lu.lu_2011_chart_1': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Malta - Done in 13.0 - CoA not available yet + 'MT': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Netherlands + 'l10n_nl.l10nnl_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Poland + 'l10n_pl.pl_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Portugal + 'l10n_pt.pt_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Romania + 'l10n_ro.ro_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Slovakia - Done in 13.0 - CoA not available yet + 'l10n_sk.sk_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Slovenia + 'l10n_si.gd_chart': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Spain + 'l10n_es.account_chart_template_common': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, + # Sweden + 'l10n_se.l10nse_chart_template': { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }, +} diff --git a/addons/l10n_eu_oss/models/res_company.py b/addons/l10n_eu_oss/models/res_company.py index 6d1149e85be..a5ef49b0d76 100644 --- a/addons/l10n_eu_oss/models/res_company.py +++ b/addons/l10n_eu_oss/models/res_company.py @@ -3,8 +3,7 @@ from odoo import api, models, Command from .eu_tax_map import EU_TAX_MAP - -import itertools +from .eu_tag_map import EU_TAG_MAP class Company(models.Model): @@ -91,21 +90,19 @@ class Company(models.Model): def _get_repartition_lines_oss(self): self.ensure_one() defaults = self.env['account.tax'].with_company(self).default_get(['invoice_repartition_line_ids', 'refund_repartition_line_ids']) - oss_account = self._get_oss_account() - if oss_account: - defaults['invoice_repartition_line_ids'][1][2]['account_id'] = oss_account.id - defaults['refund_repartition_line_ids'][1][2]['account_id'] = oss_account.id - - oss_tag = self.env.ref('l10n_eu_oss.tag_oss') - for orm_command in itertools.chain(defaults['invoice_repartition_line_ids'], defaults['refund_repartition_line_ids']): - rep_line_vals = orm_command[2] - rep_line_vals['tag_ids'] = rep_line_vals.get('tag_ids', []) + [Command.link(oss_tag.id)] - + oss_account, oss_tags = self._get_oss_account(), self._get_oss_tags() + base_line, tax_line, vals = 0, 1, 2 + for doc_type in 'invoice', 'refund': + if oss_account: + defaults[f'{doc_type}_repartition_line_ids'][tax_line][vals]['account_id'] = oss_account.id + if oss_tags: + defaults[f'{doc_type}_repartition_line_ids'][base_line][vals]['tag_ids'] += [Command.link(tag.id) for tag in oss_tags[f'{doc_type}_base_tag']] + defaults[f'{doc_type}_repartition_line_ids'][tax_line][vals]['tag_ids'] += [Command.link(tag.id) for tag in oss_tags[f'{doc_type}_tax_tag']] return defaults['invoice_repartition_line_ids'], defaults['refund_repartition_line_ids'] def _get_oss_account(self): self.ensure_one() - if not self.env.ref('l10n_eu_oss.oss_tax_account_company_%s' % self.id, raise_if_not_found=False): + if not self.env.ref(f'l10n_eu_oss.oss_tax_account_company_{self.id}', raise_if_not_found=False): sales_tax_accounts = self.env['account.tax'].search([ ('type_tax_use', '=', 'sale'), ('company_id', '=', self.id) @@ -127,3 +124,22 @@ class Company(models.Model): 'noupdate': True, }) return self.env.ref(f'l10n_eu_oss.oss_tax_account_company_{self.id}') + + def _get_oss_tags(self): + oss_tag = self.env.ref('l10n_eu_oss.tag_oss') + [chart_template_xml_id] = self.chart_template_id.get_external_id().values() + tag_for_country = EU_TAG_MAP.get(chart_template_xml_id, { + 'invoice_base_tag': None, + 'invoice_tax_tag': None, + 'refund_base_tag': None, + 'refund_tax_tag': None, + }) + + return { + repartition_line_key: ( + self.env.ref(tag_xml_id).tag_ids.filtered(lambda t: not t.tax_negate) + if tag_xml_id + else self.env['account.account.tag'] + ) + oss_tag + for repartition_line_key, tag_xml_id in tag_for_country.items() + } diff --git a/addons/l10n_eu_oss/tests/__init__.py b/addons/l10n_eu_oss/tests/__init__.py new file mode 100644 index 00000000000..d3238dc85f7 --- /dev/null +++ b/addons/l10n_eu_oss/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_oss diff --git a/addons/l10n_eu_oss/tests/test_oss.py b/addons/l10n_eu_oss/tests/test_oss.py new file mode 100644 index 00000000000..e1f0b82c5b7 --- /dev/null +++ b/addons/l10n_eu_oss/tests/test_oss.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +from odoo.addons.l10n_eu_oss.models.eu_tag_map import EU_TAG_MAP +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.tests import tagged + + +@tagged('post_install', 'post_install_l10n', '-at_install') +class TestOSSBelgium(AccountTestInvoicingCommon): + + @classmethod + def setUpClass(cls, chart_template_ref='l10n_be.l10nbe_chart_template'): + try: + super().setUpClass(chart_template_ref=chart_template_ref) + except ValueError as e: + if e.args[0] == "External ID not found in the system: l10n_be.l10nbe_chart_template": + cls.skipTest(cls, reason="Belgian CoA is required for this testSuite but l10n_be isn't installed") + else: + raise e + cls.company_data['company'].country_id = cls.env.ref('base.be') + cls.company_data['company']._map_eu_taxes() + + def test_country_tag_from_belgium(self): + # get an eu country which isn't the current one: + another_eu_country_code = (self.env.ref('base.europe').country_ids - self.company_data['company'].country_id)[0].code + tax_oss = self.env['account.tax'].search([('name', 'ilike', f'%{another_eu_country_code}%')], limit=1) + + for doc_type, report_line_xml_id in ( + ("invoice", "l10n_be.tax_report_line_47"), + ("refund", "l10n_be.tax_report_line_49"), + ): + with self.subTest(doc_type=doc_type, report_line_xml_id=report_line_xml_id): + oss_tag_id = tax_oss[f"{doc_type}_repartition_line_ids"]\ + .filtered(lambda x: x.repartition_type == 'base')\ + .tag_ids + + expected_tag_id = self.env.ref(report_line_xml_id)\ + .tag_ids\ + .filtered(lambda t: not t.tax_negate) + + self.assertIn(expected_tag_id, oss_tag_id, f"{doc_type} tag from Belgian CoA not correctly linked") + + +@tagged('post_install', 'post_install_l10n', '-at_install') +class TestOSSUSA(AccountTestInvoicingCommon): + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + cls.company_data['company'].country_id = cls.env.ref('base.us') + cls.company_data['company']._map_eu_taxes() + + def test_no_oss_tax(self): + # get an eu country which isn't the current one: + another_eu_country_code = (self.env.ref('base.europe').country_ids - self.company_data['company'].country_id)[0].code + tax_oss = self.env['account.tax'].search([('name', 'ilike', f'%{another_eu_country_code}%')], limit=1) + + self.assertFalse(len(tax_oss), "OSS tax shouldn't be instanced on a US company") + + + +@tagged('post_install', 'post_install_l10n', '-at_install') +class TestOSSMap(AccountTestInvoicingCommon): + + def test_oss_eu_tag_map(self): + """ Checks that the xml_id referenced in the map are correct. + In case of failure display the couple (chart_template_xml_id, tax_report_line_xml_id). + The test doesn't fail for unreferenced char_template or unreferenced tax_report_line. + """ + chart_templates = self.env['account.chart.template'].search([]) + for chart_template in chart_templates: + [chart_template_xml_id] = chart_template.get_external_id().values() + oss_tags = EU_TAG_MAP.get(chart_template_xml_id, {}) + for tax_report_line_xml_id in filter(lambda d: d, oss_tags.values()): + with self.subTest(chart_template_xml_id=chart_template_xml_id, tax_report_line_xml_id=tax_report_line_xml_id): + tag = self.env.ref(tax_report_line_xml_id, raise_if_not_found=False) + self.assertIsNotNone(tag, f"The following xml_id is incorrect in EU_TAG_MAP.py:{tax_report_line_xml_id}")