From b1f1f216d09e8d7ac42ad91ad5694b1eb3bdf68c Mon Sep 17 00:00:00 2001 From: "Daniel Kosky (dako)" Date: Thu, 23 Mar 2023 13:27:48 +0000 Subject: [PATCH] [IMP] base_vat: VIES validation on Fiscal Position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- addons/account/models/partner.py | 10 +- .../models/account_fiscal_position.py | 18 +++ addons/base_vat/models/res_partner.py | 126 +++++++++--------- addons/base_vat/views/res_partner_views.xml | 10 +- addons/sale_stock/tests/test_create_perf.py | 6 +- addons/test_mail/tests/test_performance.py | 8 +- 6 files changed, 100 insertions(+), 78 deletions(-) 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 - -