diff --git a/addons/partner_autocomplete/models/__init__.py b/addons/partner_autocomplete/models/__init__.py index 5d9eecc13d7..f8544fa9005 100644 --- a/addons/partner_autocomplete/models/__init__.py +++ b/addons/partner_autocomplete/models/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import iap_autocomplete_api from . import res_partner from . import res_company from . import res_config_settings diff --git a/addons/partner_autocomplete/models/iap_autocomplete_api.py b/addons/partner_autocomplete/models/iap_autocomplete_api.py new file mode 100644 index 00000000000..97e382c3a8a --- /dev/null +++ b/addons/partner_autocomplete/models/iap_autocomplete_api.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging + +from odoo import api, models, exceptions, _ +from odoo.addons.iap.tools import iap_tools +from requests.exceptions import HTTPError + +_logger = logging.getLogger(__name__) + + +class IapAutocompleteEnrichAPI(models.AbstractModel): + _name = 'iap.autocomplete.api' + _description = 'IAP Partner Autocomplete API' + _DEFAULT_ENDPOINT = 'https://partner-autocomplete.odoo.com' + + @api.model + def _contact_iap(self, local_endpoint, action, params, timeout=15): + if self.env.registry.in_test_mode(): + raise exceptions.ValidationError(_('Test mode')) + account = self.env['iap.account'].get('partner_autocomplete') + if not account.account_token: + raise ValueError(_('No account token')) + params.update({ + 'db_uuid': self.env['ir.config_parameter'].sudo().get_param('database.uuid'), + 'account_token': account.account_token, + 'country_code': self.env.company.country_id.code, + 'zip': self.env.company.zip, + }) + base_url = self.env['ir.config_parameter'].sudo().get_param('iap.partner_autocomplete.endpoint', self._DEFAULT_ENDPOINT) + return iap_tools.iap_jsonrpc(base_url + local_endpoint + '/' + action, params=params, timeout=timeout) + + @api.model + def _request_partner_autocomplete(self, action, params, timeout=15): + """ Contact endpoint to get autocomplete data. + + :return tuple: results, error code + """ + try: + results = self._contact_iap('/iap/partner_autocomplete', action, params, timeout=timeout) + except exceptions.ValidationError: + return False, 'Insufficient Credit' + except (ConnectionError, HTTPError, exceptions.AccessError, exceptions.UserError) as exception: + _logger.error('Autocomplete API error: %s', str(exception)) + return False, str(exception) + except iap_tools.InsufficientCreditError as exception: + _logger.warning('Insufficient Credits for Autocomplete Service: %s', str(exception)) + return False, 'Insufficient Credit' + except ValueError: + return False, 'No account token' + return results, False diff --git a/addons/partner_autocomplete/models/res_partner.py b/addons/partner_autocomplete/models/res_partner.py index 682789b08c3..2455ff08923 100644 --- a/addons/partner_autocomplete/models/res_partner.py +++ b/addons/partner_autocomplete/models/res_partner.py @@ -1,16 +1,13 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import logging import json -from odoo import api, fields, models, exceptions, _ -from odoo.addons.iap.tools import iap_tools -# TDE FIXME: check those errors at iap level ? -from requests.exceptions import ConnectionError, HTTPError +import logging + +from odoo import api, fields, models, _ _logger = logging.getLogger(__name__) -DEFAULT_ENDPOINT = 'https://partner-autocomplete.odoo.com' class ResPartner(models.Model): _name = 'res.partner' @@ -75,38 +72,9 @@ class ResPartner(models.Model): return country_id, state_id - @api.model - def get_endpoint(self): - url = self.env['ir.config_parameter'].sudo().get_param('iap.partner_autocomplete.endpoint', DEFAULT_ENDPOINT) - url += '/iap/partner_autocomplete' - return url - - @api.model - def _rpc_remote_api(self, action, params, timeout=15): - if self.env.registry.in_test_mode() : - return False, 'Insufficient Credit' - url = '%s/%s' % (self.get_endpoint(), action) - account = self.env['iap.account'].get('partner_autocomplete') - if not account.account_token: - return False, 'No Account Token' - params.update({ - 'db_uuid': self.env['ir.config_parameter'].sudo().get_param('database.uuid'), - 'account_token': account.account_token, - 'country_code': self.env.company.country_id.code, - 'zip': self.env.company.zip, - }) - try: - return iap_tools.iap_jsonrpc(url=url, params=params, timeout=timeout), False - except (ConnectionError, HTTPError, exceptions.AccessError, exceptions.UserError) as exception: - _logger.error('Autocomplete API error: %s' % str(exception)) - return False, str(exception) - except iap_tools.InsufficientCreditError as exception: - _logger.warning('Insufficient Credits for Autocomplete Service: %s' % str(exception)) - return False, 'Insufficient Credit' - @api.model def autocomplete(self, query, timeout=15): - suggestions, error = self._rpc_remote_api('search', { + suggestions, _ = self.env['iap.autocomplete.api']._request_partner_autocomplete('search', { 'query': query, }, timeout=timeout) if suggestions: @@ -119,7 +87,7 @@ class ResPartner(models.Model): @api.model def enrich_company(self, company_domain, partner_gid, vat, timeout=15): - response, error = self._rpc_remote_api('enrich', { + response, error = self.env['iap.autocomplete.api']._request_partner_autocomplete('enrich', { 'domain': company_domain, 'partner_gid': partner_gid, 'vat': vat, @@ -144,7 +112,7 @@ class ResPartner(models.Model): @api.model def read_by_vat(self, vat, timeout=15): - vies_vat_data, error = self._rpc_remote_api('search_vat', { + vies_vat_data, _ = self.env['iap.autocomplete.api']._request_partner_autocomplete('search_vat', { 'vat': vat, }, timeout=timeout) if vies_vat_data: diff --git a/addons/partner_autocomplete/models/res_partner_autocomplete_sync.py b/addons/partner_autocomplete/models/res_partner_autocomplete_sync.py index 8b386e29128..5152dc40c0e 100644 --- a/addons/partner_autocomplete/models/res_partner_autocomplete_sync.py +++ b/addons/partner_autocomplete/models/res_partner_autocomplete_sync.py @@ -25,7 +25,7 @@ class ResPartnerAutocompleteSync(models.Model): if partner.vat and partner._is_vat_syncable(partner.vat): params['vat'] = partner.vat - result, error = partner._rpc_remote_api('update', params) + _, error = self.env['iap.autocomplete.api']._request_partner_autocomplete('update', params) if error: _logger.error('Send Partner to sync failed: %s' % str(error)) diff --git a/addons/partner_autocomplete/tests/common.py b/addons/partner_autocomplete/tests/common.py new file mode 100644 index 00000000000..002059f7643 --- /dev/null +++ b/addons/partner_autocomplete/tests/common.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from contextlib import contextmanager +from unittest.mock import patch + +from odoo import exceptions +from odoo.addons.iap.tools import iap_tools +from odoo.addons.partner_autocomplete.models.iap_autocomplete_api import IapAutocompleteEnrichAPI +from odoo.tests import common + + +class MockIAPPartnerAutocomplete(common.BaseCase): + """ Mock PartnerAutocomplete IAP calls for testing purpose. + + Example of company_data { + 'partner_gid': 51580, 'website': 'mywebsite.be', + 'additional_info': { + "name": "Mywebsite", + "description": "Mywebsite is the largest of Belgium\'s custom companies and part of Mywebsite Group.", + "facebook": "mywebsitebe", "twitter": "mywebsite", "linkedin": "company/mywebsite", + "twitter_followers": 99999, "twitter_bio": "This is the official Twitter account of MyWebsite.", + "industry_group": "Technology Hardware & Equipment", "sub_industry": "Computer Networking", + "industry": "Communications Equipment", + "sector": ["Information Technology", "Technology Hardware & Equipment"], + "sector_primary": "Information Technology" + "tech": ["Tealium", "Hotjar", "Google Analytics", "Instagram", "Facebook Advertiser", "Facebook Connect", "Google Tag Manager", "Mandrill", "Bazaarvoice", "Mailgun", "Conversio"], + "email": [], "crunchbase": "organization/mywebsite-group", + "phone_numbers": ["+32 800 00 000", "+32 800 00 001", "+32 800 00 002"], + "timezone": "Europe/Brussels", "timezone_url": "https://time.is/Brussels", + "company_type": "private", "employees": 15000.0, "annual_revenue": 0.0, "estimated_annual_revenue": false, "founded_year": 0, + "logo": "https://logo.clearbit.com/mywebsite.be"}, + 'child_ids': [{ + 'is_company': False, 'type': 'contact', 'city': False, 'email': False, + 'name': 'Client support - SMEs', 'street': 'False False', 'phone': '0800 00 500', + 'zip': False, 'country_id': False, 'state_id': False}, { + 'is_company': False, 'type': 'contact', 'city': False, 'email': False, + 'name': 'Client Support - Large Business', 'street': 'False False', 'phone': '0800 00 501', + 'zip': False, 'country_id': False, 'state_id': False}], + 'city': 'Brussel', 'vat': 'BE0202239951', + 'email': False, 'logo': 'https://logo.clearbit.com/mywebsite.be', + 'name': 'Proximus', 'zip': '1000', 'ignored': False, 'phone': '+32 800 00 800', + 'bank_ids': [{ + 'acc_number': 'BE012012012', 'acc_holder_name': 'MyWebsite'}, { + 'acc_number': 'BE013013013', 'acc_holder_name': 'MyWebsite Online'}], + 'street': 'Rue Perdues 27', + 'country_code': 'de', 'country_name': 'Germany', + 'state_id': False + } + """ + + @classmethod + def _init_mock_partner_autocomplete(cls): + super(MockIAPPartnerAutocomplete, cls).setUpClass() + cls.base_de = cls.env.ref('base.de') + cls.base_be = cls.env.ref('base.be') + cls.be_state_bw = cls.env['res.country.state'].create({'name': 'Béwééé dis', 'code': 'bw', 'country_id': cls.base_be.id}) + + @contextmanager + def mockPartnerAutocomplete(self, default_data=None, sim_error=None): + def _contact_iap(local_endpoint, action, params, timeout): + sim_result = { + 'partner_gid': '9876', 'website': 'https://www.heinrich.de', + 'additional_info': {}, + 'city': 'Mönchengladbach', + 'email': False, 'logo': 'https://logo.clearbit.com/heinrichsroofing.com', + 'name': 'Heinrich', 'zip': '41179', 'ignored': False, 'phone': '+49 0000 112233', + 'street': 'Mennrather Str. 123456', + 'country_code': self.base_de.code, 'country_name': self.base_de.name, + 'state_id': False, + 'child_ids': [{ + 'is_company': False, 'type': 'contact', 'city': 'Orcq', + 'name': 'Heinrich Support' + }, { + 'is_company': False, 'type': 'contact', 'email': 'heinrich.clien@test.example.com', + 'name': 'Heinrich Client Support', 'street': 'Rue des Bourlottes, 9', 'phone': '0456 00 11 22', + 'zip': '1367', 'country_code': self.base_be.code, 'country_name': self.base_be.name, + 'state_code': self.be_state_bw.code, 'state_name': self.be_state_bw.name + }], + } + if default_data: + sim_result.update(default_data) + # mock enrich only currently, to update further + if action == 'enrich': + if sim_error and sim_error == 'credit': + raise iap_tools.InsufficientCreditError('InsufficientCreditError') + elif sim_error and sim_error == 'jsonrpc_exception': + raise exceptions.AccessError( + 'The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was ' + local_endpoint + ) + elif sim_error and sim_error == 'token': + raise ValueError('No account token') + return {'company_data': sim_result} + + try: + with patch.object(IapAutocompleteEnrichAPI, '_contact_iap', side_effect=_contact_iap): + yield + finally: + pass