[IMP] base_vat: VIES validation on Fiscal Position
At present the user can select an option in the settings to check VAT numbers against the VIES system. If the VAT number fails this validation the result is a non-blocking banner message that informs the user that the VIES validation has failed, but has no further ramifications (the user can still use an unrecognised VAT number). This non-blocking functionality is still desirable, however we wish to determine the validity of certain fiscal positions based on whether the VIES VAT check is valid. In order to acheive this, the computed boolean `vies_valid` field is added, and populated based on the results when comparing the VAT against the VIES system. It depends on the `vat` and `country_id` of the partner. If it looks like the VIES check needs to be performed on this vat and if any company in the db requires a VIES vat check, the check is performed, and if none do, then check is not performed. The field can be manually edited, but is also tracked. Provided we know whether a partner has a valid VIES vat or not, it is only important sometimes in trying to find the appropriate fiscal position (because VIES is only confirms validity of a VAT number for intra-community trade). Because of this, a computed boolean field called `perform_vies_validation` is added to represent this on the partner. For example if a partner is from the same country as the current company, then it doesn't matter that it's VIES valid or not, all that matters is that there is some string in its vat field for the "VAT Required" to be satsified, so the `perform_vies_validation` field would be False. This field is also used to determine whether the `vies_valid` checkbox should be shown or hidden on the partner form view. A hook called _get_vat_valid is placed in the method on fiscal position that retrieves the appropriate fiscal position for a given account move, and it is overridden by a function in base_vat, which specifies whether the partner/delivery address matches the 'vat_required' condition when VIES validity is relevant for the company/partner (see the above `perform_vies_validation` field). `sale_stock` and `test_mail` performance tests are updated in order to account for the additional queries introduced in the _get_vat_required hook (in fetching the base.europe country ids) and the _compute_vies_valid respectively. closes odoo/odoo#116391 Task-id: 3218194 Related: odoo/upgrade#4498 Signed-off-by: William André (wan) <wan@odoo.com>
This commit is contained in:
@@ -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']
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
<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"/>
|
||||
<xpath expr="//field[@name='vat']" position="replace">
|
||||
<field name="perform_vies_validation" invisible="1"/>
|
||||
<label for="vat" string="Tax ID"/>
|
||||
<div>
|
||||
<field name="vat" placeholder="e.g. BE0477472701" attrs="{'readonly': [('parent_id','!=',False)]}" class="oe_inline"/>
|
||||
<label for="vies_valid" attrs="{'invisible': [('perform_vies_validation', '=', False)]}"/>
|
||||
<field name="vies_valid" attrs="{'invisible': [('perform_vies_validation', '=', False)]}"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user