[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
This commit is contained in:
Nicolas (vin)
2023-01-30 22:27:14 +01:00
parent 339cc6fba8
commit 98a225f4d2
6 changed files with 93 additions and 44 deletions
+1
View File
@@ -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',
}
+7
View File
@@ -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
+64 -41
View File
@@ -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):
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_partner_base_vat_form" model="ir.ui.view">
<field name="model">res.partner</field>
<field name="name">view.partner.base.vat.form</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<xpath expr="//div[hasclass('alert')]" position="after">
<div class="alert alert-warning oe_edit_only" role="alert" attrs="{'invisible': [('vies_failed_message', '=', False)]}">
<field name="vies_failed_message" nolabel="1" readonly="1"/>
</div>
</xpath>
</field>
</record>
</data>
</odoo>
+3 -2
View File
@@ -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
+1 -1
View File
@@ -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,