diff --git a/base_vat_optional_vies/README.rst b/base_vat_optional_vies/README.rst new file mode 100644 index 00000000..9a1d3eea --- /dev/null +++ b/base_vat_optional_vies/README.rst @@ -0,0 +1,78 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +=================================== +Optional validation of VAT via VIES +=================================== + +This module extends base_vat module features allowing to know if VIES +validation was passed or not. + +Then you can use "VIES validation passed" field in order to show VAT ID with +or without country preffix in invoices, for instance. + +*NOTE*: Altought VIES validation is actived in your company, this validation +will not block VAT ID write (main different to Odoo standard behavior) if this +VAT ID is valid in its country. + + +Configuration +============= + +In order to activate VIES validation, you must set this option in your company: +Settings > Companies > Companies > Your Company > Configuration > Accounting > VIES VAT Check + + +Usage +===== + +When VIES VAT Check is activated: + +* Odoo will try to validate VAT against VIES online service +* If passed, then "VIES validation passed" field will be True +* If not passed, then try to validate using country validation method +* If validated, then "VIES validation passed" field will be False +* If not validated, then a ValidationError will be shown to user + +When VIES VAT Check is not activated: + +* "VIES validation passed" field will be always False + +You must preffix VAT with country code (ISO 3166-1 alpha-2) and if you want to +bypass country validation you can use "EU" code + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback +`here `_. + + +Credits +======= + +Contributors +------------ + +* Rafael Blasco +* Antonio Espinosa + + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/base_vat_optional_vies/__init__.py b/base_vat_optional_vies/__init__.py new file mode 100644 index 00000000..ab13a621 --- /dev/null +++ b/base_vat_optional_vies/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +from . import models diff --git a/base_vat_optional_vies/__openerp__.py b/base_vat_optional_vies/__openerp__.py new file mode 100644 index 00000000..08f8dfe3 --- /dev/null +++ b/base_vat_optional_vies/__openerp__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +{ + 'name': "Optional validation of VAT via VIES", + 'category': 'Accounting', + 'version': '8.0.1.0.0', + 'depends': [ + 'base_vat', + ], + 'external_dependencies': { + 'python': ['vatnumber'], + }, + 'data': [ + 'views/res_partner_view.xml', + ], + 'author': 'Antiun IngenierĂ­a S.L., ' + 'Odoo Community Association (OCA)', + 'website': 'http://www.antiun.com', + 'license': 'AGPL-3', + 'images': [], + 'installable': True, +} diff --git a/base_vat_optional_vies/i18n/.gitkeep b/base_vat_optional_vies/i18n/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/base_vat_optional_vies/models/__init__.py b/base_vat_optional_vies/models/__init__.py new file mode 100644 index 00000000..31fca90e --- /dev/null +++ b/base_vat_optional_vies/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +from . import res_partner diff --git a/base_vat_optional_vies/models/res_partner.py b/base_vat_optional_vies/models/res_partner.py new file mode 100644 index 00000000..20c7668e --- /dev/null +++ b/base_vat_optional_vies/models/res_partner.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +import logging +import re +from openerp import models, fields, api +from openerp.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + +try: + import vatnumber +except ImportError: + _logger.warning( + "VAT validation partially unavailable because the `vatnumber` Python " + "library cannot be found. Install it to support more countries, " + "for example with `easy_install vatnumber` or " + "`pip install vatnumber`.") + vatnumber = None + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + vies_passed = fields.Boolean( + string="VIES validation passed", readonly=True) + + def __init__(self, pool, cr): + super(ResPartner, self).__init__(pool, cr) + self._constraints = [] + + @api.constrains('vat') + def check_vat(self): + for partner in self: + if (not self.env.context.get('avoid_check_vat') and + not partner.parent_id): + if not partner.validate_vat(): + raise ValidationError(partner._construct_constraint_msg()) + + @api.multi + def button_check_vat(self): + if not self.validate_vat(): + raise ValidationError(self._construct_constraint_msg()) + return True + + def _split_vat(self, vat, country=False): + """ + @summary: Split Partner vat into country_code and number + @result: (vat_country, vat_number) + """ + vat_country = 'XX' + vat_number = vat + if vat and re.match(r'[A-Za-z]{2}', vat): + vat_country = vat[:2].upper() + vat_number = vat[2:].replace(' ', '') + elif country: + vat_country = country + return vat_country, vat_number + + @api.multi + def validate_vat(self): + self.ensure_one() + if self.company_id.vat_check_vies: + # VIES online check + check_func = self.vies_vat_optional_check + else: + # quick and partial off-line checksum validation + check_func = self.simple_vat_optional_check + vat_country, vat_number = self._split_vat(self.vat) + if vat_number and vat_country == 'XX': + _logger.info("VAT country not found!") + raise ValidationError(self._construct_constraint_msg()) + if vat_number and not check_func(vat_country, vat_number): + _logger.info("VAT Number [%s] is not valid !" % vat_number) + return False + return True + + @api.multi + def simple_vat_optional_check(self, country_code, vat_number): + """ + Check the VAT number depending of the country. + http://sima-pc.com/nif.php + """ + self.ensure_one() + res = self.simple_vat_check(country_code.lower(), vat_number) + data = {} + if res and self.vies_passed and not self.company_id.vat_check_vies: + # Can not be sure that this VAT is signed up in VIES + data['vies_passed'] = False + if res: + vat = country_code + vat_number + if self.vat != vat: + data['vat'] = vat + if data: + self.with_context(avoid_check_vat=True).write(data) + return res + + @api.multi + def vies_vat_optional_check(self, country_code, vat_number): + self.ensure_one() + data = {} + res = False + try: + # Validate against VAT Information Exchange System (VIES) + # see also http://ec.europa.eu/taxation_customs/vies/ + vat = country_code + vat_number + res = vatnumber.check_vies(vat) + if res and not self.vies_passed: + data['vies_passed'] = True + 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 fall back to the simple check. + pass + + if not res: + res = self.simple_vat_optional_check(country_code, vat_number) + if self.vies_passed: + data['vies_passed'] = False + if res: + vat = country_code + vat_number + if self.vat != vat: + data['vat'] = vat + if data: + self.with_context(avoid_check_vat=True).write(data) + return res + + # Delete old api constraint defined in base_vat addon + @api.multi + def _validate_fields(self, field_names): + self._constraints = [x for x in self._constraints if 'vat' not in x[2]] + super(ResPartner, self)._validate_fields(field_names) diff --git a/base_vat_optional_vies/static/description/icon.png b/base_vat_optional_vies/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/base_vat_optional_vies/static/description/icon.png differ diff --git a/base_vat_optional_vies/tests/__init__.py b/base_vat_optional_vies/tests/__init__.py new file mode 100644 index 00000000..64a6a530 --- /dev/null +++ b/base_vat_optional_vies/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +from . import test_res_partner diff --git a/base_vat_optional_vies/tests/test_res_partner.py b/base_vat_optional_vies/tests/test_res_partner.py new file mode 100644 index 00000000..50dbd2e4 --- /dev/null +++ b/base_vat_optional_vies/tests/test_res_partner.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +from openerp.tests.common import TransactionCase + + +class TestResPartner(TransactionCase): + + def setUp(self): + super(TestResPartner, self).setUp() + self.m_partner = self.env['res.partner'] + self.m_company = self.env['res.company'] + self.company = self.m_company.browse(self.ref('base.main_company')) + self.partner = self.m_partner.browse(self.ref('base.res_partner_1')) + + def test_split_vat(self): + cases = ( + # vat, country, => vat_country, vat_number + ('ESB12345678', False, 'ES', 'B12345678'), + ('B12345678', False, 'XX', 'B12345678'), + ('1EB12345678', False, 'XX', '1EB12345678'), + ('ESB12345678', 'DE', 'ES', 'B12345678'), + ('B12345678', 'ES', 'ES', 'B12345678'), + ) + for vat, country, vat_country, vat_number in cases: + res_country, res_number = self.m_partner._split_vat(vat, country) + self.assertEqual(res_country, vat_country) + self.assertEqual(res_number, vat_number) + + def _test_validate_vat(self, cases): + for vat, res_vat, res_vies in cases: + self.partner.write({ + 'vat': vat, + }) + self.assertEqual(self.partner.vat, res_vat) + self.assertEqual(self.partner.vies_passed, res_vies) + + # AEA: Can't use this test in Travis, VIES checking returns always False + # because of timeout + # def test_validate_vat_vies(self): + # """ + # Validate VAT when company 'vat_check_vies' option is True + # All VATs are valid, but some are not signed up in VIES database + # """ + # self.company.vat_check_vies = True + # cases = ( + # # vat => vat, vies_passed + # # VATs signed up in VIES + # ('ESB84718550', 'ESB84718550', True), + # ('de222070543', 'DE222070543', True), + # # Valid VATs don't signed up in VIES + # ('DE253130868', 'DE253130868', False), + # ('esB87286357', 'ESB87286357', False), + # ) + # self._test_validate_vat(cases) + + def test_validate_vat_no_vies(self): + """ + Validate VAT when company 'vat_check_vies' option is False + """ + self.company.vat_check_vies = False + cases = ( + # vat => vat, vies_passed + ('ESB84718550', 'ESB84718550', False), + ('de222070543', 'DE222070543', False), + ('DE253130868', 'DE253130868', False), + ('esB87286357', 'ESB87286357', False), + ) + self._test_validate_vat(cases) diff --git a/base_vat_optional_vies/views/res_partner_view.xml b/base_vat_optional_vies/views/res_partner_view.xml new file mode 100644 index 00000000..e5880eb7 --- /dev/null +++ b/base_vat_optional_vies/views/res_partner_view.xml @@ -0,0 +1,18 @@ + + + + + + Add VAT country and VIES passed fields + res.partner + + + + + + + + + + +