From 98a225f4d2fb19be70513de521f8a7b4bbeb44e4 Mon Sep 17 00:00:00 2001 From: "Nicolas (vin)" Date: Thu, 19 Jan 2023 06:25:02 +0000 Subject: [PATCH] [FIX] base_vat: VIES check should not be blocking To that end, we will now do the VIES and regular VAT check separately. The VAT check stays as a constrains, while the VIES check will now be done in an onchange and simply display a warning if it fails. We will also now allow VAT with a single character to ignore the checks. This will allow users to better distinguish partners for which they didn't enter VAT against partners which are not subject to VAT by setting the later's VAT to '/' or any other characters. Task id #3138441 X-original-commit: eff3b140cc88dd48b77948a577f195f2e1910fd8 Part-of: odoo/odoo#111324 --- addons/base_vat/__manifest__.py | 1 + addons/base_vat/i18n/base_vat.pot | 7 ++ addons/base_vat/models/res_partner.py | 105 ++++++++++++-------- addons/base_vat/views/res_partner_views.xml | 17 ++++ odoo/addons/base/i18n/base.pot | 5 +- odoo/addons/base/models/res_partner.py | 2 +- 6 files changed, 93 insertions(+), 44 deletions(-) create mode 100644 addons/base_vat/views/res_partner_views.xml diff --git a/addons/base_vat/__manifest__.py b/addons/base_vat/__manifest__.py index c7dab01330b..b4606cf2f4c 100644 --- a/addons/base_vat/__manifest__.py +++ b/addons/base_vat/__manifest__.py @@ -37,6 +37,7 @@ only the country code will be validated. 'depends': ['account'], 'data': [ 'views/res_config_settings_views.xml', + 'views/res_partner_views.xml', ], 'license': 'LGPL-3', } diff --git a/addons/base_vat/i18n/base_vat.pot b/addons/base_vat/i18n/base_vat.pot index df262aa0373..1f2c7814516 100644 --- a/addons/base_vat/i18n/base_vat.pot +++ b/addons/base_vat/i18n/base_vat.pot @@ -83,6 +83,13 @@ msgid "" "%(expected_format)s." msgstr "" +#. module: base_vat +#. odoo-python +#: code:addons/base_vat/models/res_partner.py:0 +#, python-format +msgid "The VAT number %s failed the VIES VAT validation check." +msgstr "" + #. module: base_vat #: code:addons/base_vat/models/account_fiscal_position.py:0 #, python-format diff --git a/addons/base_vat/models/res_partner.py b/addons/base_vat/models/res_partner.py index 52f2edaefb9..d2ded0fc7b4 100644 --- a/addons/base_vat/models/res_partner.py +++ b/addons/base_vat/models/res_partner.py @@ -86,6 +86,8 @@ _region_specific_vat_codes = { class ResPartner(models.Model): _inherit = 'res.partner' + vies_failed_message = fields.Char('Technical field display a message to the user if the VIES check fails.', store=False) + def _split_vat(self, vat): vat_country, vat_number = vat[:2].lower(), vat[2:].replace(' ', '') return vat_country, vat_number @@ -131,9 +133,9 @@ class ResPartner(models.Model): # Fault code may contain INVALID_INPUT, SERVICE_UNAVAILABLE, MS_UNAVAILABLE, # TIMEOUT or SERVER_BUSY. There is no way we can validate the input # with VIES if any of these arise, including the first one (it means invalid - # country code or empty VAT number), so we fall back to the simple check. + # country code or empty VAT number), so we return True and ignore the result. _logger.exception("Failed VIES VAT check.") - return self.simple_vat_check(country_code, vat_number) + return True @api.model def fix_eu_vat_number(self, country_id, vat): @@ -156,41 +158,82 @@ class ResPartner(models.Model): return for partner in self: + # Skip checks when only one character is used. Some users like to put '/' or other as VAT to differentiate between + # A partner for which they didn't input VAT, and the one not subject to VAT + if not partner.vat or len(partner.vat) == 1: + continue country = partner.commercial_partner_id.country_id - if partner.vat and self._run_vat_test(partner.vat, country, partner.is_company) is False: + if self._run_vat_test(partner.vat, country, partner.is_company) is False: partner_label = _("partner [%s]", partner.name) msg = partner._build_vat_error_message(country and country.code.lower() or None, partner.vat, partner_label) raise ValidationError(msg) + @api.onchange('vat', 'country_id') + def _onchange_check_vies(self): + """ Check the VAT number with VIES, if enabled. Return a non-blocking warning if the check fails.""" + if self.env.context.get('company_id'): + company = self.env['res.company'].browse(self.env.context['company_id']) + else: + company = self.env.company + if not company.vat_check_vies: + return + + eu_countries = self.env.ref('base.europe').country_ids + for eu_partner_company in self.filtered(lambda partner: partner.country_id in eu_countries and partner.is_company): + # Skip checks when only one character is used. Some users like to put '/' or other as VAT to differentiate between + # A partner for which they didn't input VAT, and the one not subject to VAT + if not eu_partner_company.vat or len(eu_partner_company.vat) == 1: + continue + country = eu_partner_company.country_id + if self._run_vies_test(eu_partner_company.vat, country) is False: + self.vies_failed_message = _("The VAT number %s failed the VIES VAT validation check.", eu_partner_company.vat) + else: + self.vies_failed_message = False + @api.model def _run_vat_test(self, vat_number, default_country, partner_is_company=True): - """ Checks a VAT number, either syntactically or using VIES, depending - on the active company's configuration. + """ Checks a VAT number syntactically to ensure its validity upon saving. A first check is made by using the first two characters of the VAT as - the country code. It it fails, a second one is made using default_country instead. + the country code. If it fails, a second one is made using default_country instead. :param vat_number: a string with the VAT number to check. :param default_country: a res.country object - :param partner_is_company: True if the partner is a company, else False + :param partner_is_company: True if the partner is a company, else False. + .. deprecated:: 16.0 + Will be removed in 16.2 :return: The country code (in lower case) of the country the VAT number was validated for, if it was validated. False if it could not be validated against the provided or guessed country. None if no country was available for the check, and no conclusion could be made with certainty. """ - # Get company - if self.env.context.get('company_id'): - company = self.env['res.company'].browse(self.env.context['company_id']) - else: - company = self.env.company + check_result = None - # Get check function: either simple syntactic check or call to VIES service - eu_countries = self.env.ref('base.europe').country_ids - if company.vat_check_vies and default_country in eu_countries and partner_is_company: - check_func = self.vies_vat_check - else: - check_func = self.simple_vat_check + # First check with country code as prefix of the TIN + vat_country_code, vat_number_split = self._split_vat(vat_number) + vat_has_legit_country_code = self.env['res.country'].search([('code', '=', vat_country_code.upper())], limit=1) + if not vat_has_legit_country_code: + vat_has_legit_country_code = vat_country_code.lower() in _region_specific_vat_codes + if vat_has_legit_country_code: + check_result = self.simple_vat_check(vat_country_code, vat_number_split) + if check_result: + return vat_country_code + # If it fails, check with default_country (if it exists) + if default_country: + check_result = self.simple_vat_check(default_country.code.lower(), vat_number) + if check_result: + return default_country.code.lower() + + # We allow any number if it doesn't start with a country code and the partner has no country. + # This is necessary to support an ORM limitation: setting vat and country_id together on a company + # triggers two distinct write on res.partner, one for each field, both triggering this constraint. + # If vat is set before country_id, the constraint must not break. + return check_result + + @api.model + def _run_vies_test(self, vat_number, default_country): + """ Validate a VAT number using the VIES VAT validation. """ check_result = None # First check with country code as prefix of the TIN @@ -199,20 +242,16 @@ class ResPartner(models.Model): if not vat_has_legit_country_code: vat_has_legit_country_code = vat_country_code.lower() in _region_specific_vat_codes if vat_has_legit_country_code: - check_result = check_func(vat_country_code, vat_number_split) + check_result = self.vies_vat_check(vat_country_code, vat_number_split) if check_result: return vat_country_code # If it fails, check with default_country (if it exists) if default_country: - check_result = check_func(default_country.code.lower(), vat_number) + check_result = self.vies_vat_check(default_country.code.lower(), vat_number) if check_result: return default_country.code.lower() - # We allow any number if it doesn't start with a country code and the partner has no country. - # This is necessary to support an ORM limitation: setting vat and country_id together on a company - # triggers two distinct write on res.partner, one for each field, both triggering this constraint. - # If vat is set before country_id, the constraint must not break. return check_result @api.model @@ -225,23 +264,6 @@ class ResPartner(models.Model): vat_label = company.country_id.vat_label if company.country_id else "Tax ID" expected_format = _ref_vat.get(country_code, "'CC##' (CC=Country Code, ##=VAT Number)") - if company.vat_check_vies: - if 'False' not in record_label: - return '\n' + _( - "The %(vat_label)s number [%(wrong_vat)s] for %(record_label)s either failed the VIES VAT validation check or did not respect the expected format %(expected_format)s.", - vat_label=vat_label, - wrong_vat=wrong_vat, - record_label=record_label, - expected_format=expected_format, - ) - else: - return '\n' + _( - "The %(vat_label)s number [%(wrong_vat)s] either failed the VIES VAT validation check or did not respect the expected format %(expected_format)s.", - vat_label=vat_label, - wrong_vat=wrong_vat, - expected_format=expected_format, - ) - # Catch use case where the record label is about the public user (name: False) if 'False' not in record_label: return '\n' + _( @@ -259,6 +281,7 @@ class ResPartner(models.Model): expected_format=expected_format, ) + __check_vat_ch_re = re.compile(r'E([0-9]{9}|-[0-9]{3}\.[0-9]{3}\.[0-9]{3})(MWST|TVA|IVA)$') def check_vat_ch(self, vat): diff --git a/addons/base_vat/views/res_partner_views.xml b/addons/base_vat/views/res_partner_views.xml new file mode 100644 index 00000000000..0062fced5ba --- /dev/null +++ b/addons/base_vat/views/res_partner_views.xml @@ -0,0 +1,17 @@ + + + + + res.partner + view.partner.base.vat.form + + + + + + + + + diff --git a/odoo/addons/base/i18n/base.pot b/odoo/addons/base/i18n/base.pot index 2399e9f8406..5ca086dbc49 100644 --- a/odoo/addons/base/i18n/base.pot +++ b/odoo/addons/base/i18n/base.pot @@ -20852,8 +20852,9 @@ msgstr "" #: model:ir.model.fields,help:base.field_res_partner__vat #: model:ir.model.fields,help:base.field_res_users__vat msgid "" -"The Tax Identification Number. Complete it if the contact is subjected to " -"government taxes. Used in some legal statements." +"The Tax Identification Number. Values here will be validated based on the " +"country format. You can use '/' to indicate that the partner is not subject " +"to tax." msgstr "" #. module: base diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index f30fe371cc9..4ecddf61cf9 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -192,7 +192,7 @@ class Partner(models.Model): precompute=True, # avoid queries post-create readonly=False, store=True, help='The internal user in charge of this contact.') - vat = fields.Char(string='Tax ID', index=True, help="The Tax Identification Number. Complete it if the contact is subjected to government taxes. Used in some legal statements.") + vat = fields.Char(string='Tax ID', index=True, help="The Tax Identification Number. Values here will be validated based on the country format. You can use '/' to indicate that the partner is not subject to tax.") same_vat_partner_id = fields.Many2one('res.partner', string='Partner with same Tax ID', compute='_compute_same_vat_partner_id', store=False) same_company_registry_partner_id = fields.Many2one('res.partner', string='Partner with same Company Registry', compute='_compute_same_vat_partner_id', store=False) company_registry = fields.Char(string="Company ID", compute='_compute_company_registry', store=True, readonly=False,