diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py
index 8301370bd8d..50b5d9034cf 100644
--- a/addons/account/models/partner.py
+++ b/addons/account/models/partner.py
@@ -209,6 +209,10 @@ class AccountFiscalPosition(models.Model):
fpos = self.search(base_domain + null_country_dom, limit=1)
return fpos
+ def _get_vat_valid(self, delivery, company=None):
+ """ Hook for determining VAT validity with more complex VAT requirements """
+ return bool(delivery.vat)
+
@api.model
def _get_fiscal_position(self, partner, delivery=None):
"""
@@ -235,11 +239,11 @@ class AccountFiscalPosition(models.Model):
return manual_fiscal_position
# First search only matching VAT positions
- vat_required = bool(partner.vat)
- fp = self._get_fpos_by_region(delivery.country_id.id, delivery.state_id.id, delivery.zip, vat_required)
+ vat_valid = self._get_vat_valid(delivery, company)
+ fp = self._get_fpos_by_region(delivery.country_id.id, delivery.state_id.id, delivery.zip, vat_valid)
# Then if VAT required found no match, try positions that do not require it
- if not fp and vat_required:
+ if not fp and vat_valid:
fp = self._get_fpos_by_region(delivery.country_id.id, delivery.state_id.id, delivery.zip, False)
return fp or self.env['account.fiscal.position']
diff --git a/addons/base_vat/models/account_fiscal_position.py b/addons/base_vat/models/account_fiscal_position.py
index ac07c0df4ab..f0619c97426 100644
--- a/addons/base_vat/models/account_fiscal_position.py
+++ b/addons/base_vat/models/account_fiscal_position.py
@@ -22,3 +22,21 @@ class AccountFiscalPosition(models.Model):
fp_label = _("fiscal position [%s]", record.name)
error_message = self.env['res.partner']._build_vat_error_message(record.country_id.code.lower(), record.foreign_vat, fp_label)
raise ValidationError(error_message)
+
+ def _get_vat_valid(self, delivery, company=None):
+ eu_countries = self.env.ref('base.europe').country_ids
+
+ # If VIES validation does not apply to this partner (e.g. they
+ # are in the same country as the partner), then skip.
+ if not (company and delivery.with_company(company).perform_vies_validation):
+ return super()._get_vat_valid(delivery, company)
+
+ # If the company has a fiscal position with a foreign vat in Europe, in the same country as the partner, then the VIES validity applies
+ if self.search_count([
+ ('foreign_vat', '!=', False),
+ ('country_id', '=', delivery.country_id.id),
+ ('company_id', '=', company.id)
+ ]) or company.country_id in eu_countries:
+ return super()._get_vat_valid(delivery, company) and delivery.vies_valid
+
+ return super()._get_vat_valid(delivery, company)
diff --git a/addons/base_vat/models/res_partner.py b/addons/base_vat/models/res_partner.py
index 5c65cb432bb..b5f6076d8cf 100644
--- a/addons/base_vat/models/res_partner.py
+++ b/addons/base_vat/models/res_partner.py
@@ -87,7 +87,16 @@ _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)
+ vies_valid = fields.Boolean(
+ string="Intra-Community Valid",
+ compute='_compute_vies_valid', store=True, readonly=False,
+ tracking=True,
+ help='European VAT numbers are automatically checked on the VIES database.',
+ )
+ # Field representing whether vies_valid is relevant for selecting a fiscal position on this partner
+ perform_vies_validation = fields.Boolean(compute='_compute_perform_vies_validation')
+ # Technical field used to determine the VAT to check
+ vies_vat_to_check = fields.Char(compute='_compute_vies_vat_to_check')
def _split_vat(self, vat):
vat_country, vat_number = vat[:2].lower(), vat[2:].replace(' ', '')
@@ -113,30 +122,37 @@ class ResPartner(models.Model):
return bool(self.env['res.country'].search([('code', '=ilike', country_code)]))
return check_func(vat_number)
- @api.model
- @tools.ormcache('vat')
- def _check_vies(self, vat):
- # Store the VIES result in the cache. In case an exception is raised during the request
- # (e.g. service unavailable), the fallback on simple_vat_check is not kept in cache.
- return check_vies(vat)
+ @api.depends('vat', 'country_id')
+ def _compute_vies_vat_to_check(self):
+ """ Retrieve the VAT number, if one such exists, to be used when checking against the VIES system """
+ eu_country_codes = self.env.ref('base.europe').country_ids.mapped('code')
+ 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 haven't yet input VAT, and one not subject to VAT
+ if not partner.vat or len(partner.vat) == 1:
+ partner.vies_vat_to_check = ''
+ continue
+ country_code, number = partner._split_vat(partner.vat)
+ if not country_code.isalpha() and partner.country_id:
+ country_code = partner.country_id.code
+ number = partner.vat
+ partner.vies_vat_to_check = (
+ country_code.upper() in eu_country_codes or
+ country_code.lower() in _region_specific_vat_codes
+ ) and self._fix_vat_number(country_code + number, partner.country_id.id) or ''
- @api.model
- def vies_vat_check(self, country_code, vat_number):
- try:
- # Validate against VAT Information Exchange System (VIES)
- # see also http://ec.europa.eu/taxation_customs/vies/
- vies_result = self._check_vies(country_code.upper() + vat_number)
- return vies_result['valid']
- except InvalidComponent:
- return False
- except Exception:
- # see http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl
- # 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 return True and ignore the result.
- _logger.exception("Failed VIES VAT check.")
- return True
+ @api.depends_context('company')
+ @api.depends('vies_vat_to_check')
+ def _compute_perform_vies_validation(self):
+ """ Determine whether to show VIES validity on the current VAT number """
+ for partner in self:
+ to_check = partner.vies_vat_to_check
+ company_code = self.env.company.account_fiscal_country_id.code
+ partner.perform_vies_validation = (
+ to_check
+ and not to_check[:2].upper() == company_code
+ and self.env.company.vat_check_vies
+ )
@api.model
def fix_eu_vat_number(self, country_id, vat):
@@ -169,27 +185,30 @@ class ResPartner(models.Model):
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:
+ @api.depends('vies_vat_to_check')
+ def _compute_vies_valid(self):
+ """ Check the VAT number with VIES, if enabled."""
+ if not self.env['res.company'].sudo().search_count([('vat_check_vies', '=', True)]):
+ self.vies_valid = False
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:
+ for partner in self:
+ if not partner.vies_vat_to_check:
+ partner.vies_valid = False
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
+ try:
+ vies_valid = check_vies(partner.vies_vat_to_check, timeout=10)
+ partner.vies_valid = vies_valid['valid']
+ except Exception as e:
+ if partner._origin.id:
+ msg = ""
+ if isinstance(e, OSError):
+ msg = _("Connection with the VIES server failed. The VAT number %s could not be validated.", partner.vies_vat_to_check)
+ elif isinstance(e, InvalidComponent):
+ msg = _("The VAT number %s could not be interpreted by the VIES server.", partner.vies_vat_to_check)
+ partner._origin.message_post(body=msg)
+ _logger.exception("The VAT number %s failed VIES check.", partner.vies_vat_to_check)
+ partner.vies_valid = False
@api.model
def _run_vat_test(self, vat_number, default_country, partner_is_company=True):
@@ -218,29 +237,6 @@ class ResPartner(models.Model):
# 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
- 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())])
- 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.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 = self.vies_vat_check(default_country.code.lower(), vat_number)
- if check_result:
- return default_country.code.lower()
-
- return check_result
-
@api.model
def _build_vat_error_message(self, country_code, wrong_vat, record_label):
# OVERRIDE account
diff --git a/addons/base_vat/views/res_partner_views.xml b/addons/base_vat/views/res_partner_views.xml
index 0062fced5ba..c7da66798c3 100644
--- a/addons/base_vat/views/res_partner_views.xml
+++ b/addons/base_vat/views/res_partner_views.xml
@@ -6,9 +6,13 @@
view.partner.base.vat.form
-
-
-
+
+
+
+
+
+
+
diff --git a/addons/sale_stock/tests/test_create_perf.py b/addons/sale_stock/tests/test_create_perf.py
index bea0b7a33c6..89bd3f382c5 100644
--- a/addons/sale_stock/tests/test_create_perf.py
+++ b/addons/sale_stock/tests/test_create_perf.py
@@ -50,7 +50,7 @@ class TestPERF(common.TransactionCase):
@warmup
@prepare
def test_empty_sale_order_creation_perf(self):
- with self.assertQueryCount(admin=31):
+ with self.assertQueryCount(admin=32):
self.env['sale.order'].create({
'partner_id': self.partners[0].id,
'user_id': self.salesmans[0].id,
@@ -65,7 +65,7 @@ class TestPERF(common.TransactionCase):
# + 1 warehouse fetch
# + 1 query to get analytic default account
# + 1 followers queries ?
- with self.assertQueryCount(admin=35):
+ with self.assertQueryCount(admin=36):
self.env['sale.order'].create([{
'partner_id': self.partners[0].id,
'user_id': self.salesmans[0].id,
@@ -77,7 +77,7 @@ class TestPERF(common.TransactionCase):
def test_dummy_sales_orders_batch_creation_perf(self):
""" Dummy SOlines (notes/sections) should not add any custom queries other than their insert"""
# + 2 SOL (batched) insert
- with self.assertQueryCount(admin=37):
+ with self.assertQueryCount(admin=39):
self.env['sale.order'].create([{
'partner_id': self.partners[0].id,
'user_id': self.salesmans[0].id,
diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py
index 05ce38169e4..c2e6ec356f5 100644
--- a/addons/test_mail/tests/test_performance.py
+++ b/addons/test_mail/tests/test_performance.py
@@ -455,7 +455,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
test_record, test_template = self._create_test_records()
test_template.write({'attachment_ids': [(5, 0)]})
- with self.assertQueryCount(admin=25, employee=25): # tm 15/15 / com 24/24
+ with self.assertQueryCount(admin=26, employee=26): # tm 15/15 / com 24/24
composer = self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
'default_model': test_record._name,
@@ -479,7 +479,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
def test_mail_composer_w_template_attachments(self):
test_record, test_template = self._create_test_records()
- with self.assertQueryCount(admin=25, employee=25): # tm 15/15 / com 24/24
+ with self.assertQueryCount(admin=26, employee=26): # tm 15/15 / com 24/24
composer = self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
'default_model': test_record._name,
@@ -508,7 +508,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
test_template.write({'attachment_ids': [(5, 0)]})
customer = self.env['res.partner'].browse(self.customer.ids)
- with self.assertQueryCount(admin=38, employee=38): # tm 28/28 / com 37/37
+ with self.assertQueryCount(admin=39, employee=39): # tm 28/28 / com 37/37
composer_form = Form(
self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
@@ -538,7 +538,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
test_record, test_template = self._create_test_records()
customer = self.env['res.partner'].browse(self.customer.ids)
- with self.assertQueryCount(admin=37, employee=37): # tm 27/27 / com 36/36
+ with self.assertQueryCount(admin=38, employee=38): # tm 27/27 / com 36/36
composer_form = Form(
self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',