diff --git a/modules/party/CHANGELOG b/modules/party/CHANGELOG index 947b50aad0..1d1aa6697b 100644 --- a/modules/party/CHANGELOG +++ b/modules/party/CHANGELOG @@ -1,3 +1,4 @@ +* Autocomplete party from European VAT number * Use vat alias for party identifier * Add Belgian BIS Number identifier * Add Belgian National Number identifier diff --git a/modules/party/party.py b/modules/party/party.py index d915b560b5..93c28a3368 100644 --- a/modules/party/party.py +++ b/modules/party/party.py @@ -1,10 +1,14 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. + +import logging + import stdnum.exceptions from sql import Column, Literal from sql.aggregate import Min from sql.functions import CharLength from stdnum import get_cc_module +from stdnum.eu.vat import MEMBER_STATES as EU_MEMBER_STATES from trytond.i18n import gettext from trytond.model import ( @@ -21,6 +25,8 @@ from .contact_mechanism import _PHONE_TYPES, _ContactMechanismMixin from .exceptions import ( EraseError, InvalidIdentifierCode, SimilarityWarning, VIESUnavailable) +logger = logging.getLogger(__name__) + class Party( DeactivableMixin, _ContactMechanismMixin, ModelSQL, ModelView, @@ -322,6 +328,86 @@ class Party( return address return default_address + @classmethod + def autocomplete(cls, text, domain=None, limit=None, order=None): + pool = Pool() + Configuration = pool.get('party.configuration') + configuration = Configuration(1) + + result = super().autocomplete( + text, domain=domain, limit=limit, order=order) + + identifier_types = dict(configuration.get_identifier_types()) + + def add(type, code): + result.append({ + 'id': None, + 'name': code, + 'defaults': { + 'identifiers': [{ + 'type': type, + 'code': code, + }], + }, + }) + + eu_vat = get_cc_module('eu', 'vat') + if 'eu_vat' in identifier_types and eu_vat.is_valid(text): + code = eu_vat.compact(text) + add('eu_vat', code) + else: + for country in eu_vat.guess_country(text): + if 'eu_vat' in identifier_types: + code = eu_vat.compact(country + text) + add('eu_vat', code) + elif f'{country}_vat' in identifier_types: + vat = get_cc_module(country, 'vat') + code = vat.compact(text) + add(f'{country}_vat', code) + return result + + @classmethod + def default_get(cls, fields_names, with_rec_name=True): + pool = Pool() + Country = pool.get('country.country') + + values = super().default_get(fields_names, with_rec_name=with_rec_name) + eu_vat = get_cc_module('eu', 'vat') + eu_types = {'eu_vat'} | {f'{m}_vat' for m in EU_MEMBER_STATES} + for identifier in values.get('identifiers') or []: + if identifier.get('type') in eu_types and identifier.get('code'): + code = identifier['code'] + if identifier['type'] != 'eu_vat': + code = identifier['type'][2:] + code + try: + result = eu_vat.check_vies(code) + except Exception: + print('exception', code) + logger.debug( + f"Fail to check {identifier['code']}", exc_info=True) + continue + if result['valid']: + values['name'] = result['name'] + for address in values.get('addresses') or []: + if not address: + break + else: + address = {} + values.setdefault('addresses', []).insert(0, address) + address['street'] = result['address'] + country_code = code[:2] + countries = Country.search([ + ('code', 'ilike', country_code), + ], limit=1) + if countries: + country, = countries + address['country'] = country.id + if with_rec_name: + address.setdefault('country.', {})['rec_name'] = ( + country.rec_name) + break + return values + class PartyLang(ModelSQL, ValueMixin): "Party Lang" diff --git a/modules/party/tests/test_module.py b/modules/party/tests/test_module.py index 3a3ee599ef..d800b38b5a 100644 --- a/modules/party/tests/test_module.py +++ b/modules/party/tests/test_module.py @@ -1,6 +1,9 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest +from unittest.mock import patch + +from stdnum import get_cc_module try: import phonenumbers @@ -100,6 +103,102 @@ class PartyTestCase(PartyCheckEraseMixin, ModuleTestCase): 'code': code, }) + @with_transaction() + def test_party_autocomplete_eu_vat(self): + "Test party autocomplete eu_vat" + pool = Pool() + Party = pool.get('party.party') + + self.assertEqual( + Party.autocomplete('BE500923836'), [{ + 'id': None, + 'name': 'BE0500923836', + 'defaults': { + 'identifiers': [{ + 'type': 'eu_vat', + 'code': 'BE0500923836', + }], + }, + }]) + + @with_transaction() + def test_party_autocomplete_eu_vat_without_country(self): + "Test party autocomplete eu_vat without country" + pool = Pool() + Party = pool.get('party.party') + + self.assertIn({ + 'id': None, + 'name': 'BE0500923836', + 'defaults': { + 'identifiers': [{ + 'type': 'eu_vat', + 'code': 'BE0500923836', + }], + }, + }, + Party.autocomplete('500923836')) + + @with_transaction() + def test_party_autocomplete_be_vat(self): + "Test party autocomplete be_vat" + pool = Pool() + Party = pool.get('party.party') + Configuration = pool.get('party.configuration') + + configuration = Configuration(1) + configuration.identifier_types = ['be_vat'] + configuration.save() + + self.assertEqual( + Party.autocomplete('BE500923836'), [{ + 'id': None, + 'name': '0500923836', + 'defaults': { + 'identifiers': [{ + 'type': 'be_vat', + 'code': '0500923836', + }], + }, + }]) + + @with_transaction() + def test_party_default_get_eu_vat(self): + "Test party default_get eu_vat" + pool = Pool() + Party = pool.get('party.party') + Country = pool.get('country.country') + + belgium = Country(code='BE', name="Belgium") + belgium.save() + + eu_vat = get_cc_module('eu', 'vat') + with patch.object(eu_vat, 'check_vies') as check_vies: + check_vies.return_value = { + 'valid': True, + 'name': "Tryton Foundation", + 'address': "Street", + } + with Transaction().set_context(default_identifiers=[{ + 'type': 'eu_vat', + 'code': 'BE0500923836', + }]): + self.assertEqual( + Party.default_get(['name', 'addresses', 'identifiers']), { + 'name': "Tryton Foundation", + 'addresses': [{ + 'street': "Street", + 'country': belgium.id, + 'country.': { + 'rec_name': "🇧🇪 Belgium", + }, + }], + 'identifiers': [{ + 'type': 'eu_vat', + 'code': 'BE0500923836', + }], + }) + @with_transaction() def test_address(self): 'Create address'