diff --git a/addons/crm_iap_lead/static/img/crunchbase.ico b/addons/crm_iap_lead/static/img/crunchbase.ico
new file mode 100644
index 00000000000..aa710c85b90
Binary files /dev/null and b/addons/crm_iap_lead/static/img/crunchbase.ico differ
diff --git a/addons/crm_iap_lead_enrich/__init__.py b/addons/crm_iap_lead_enrich/__init__.py
new file mode 100644
index 00000000000..90b17558ffc
--- /dev/null
+++ b/addons/crm_iap_lead_enrich/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
+from . import tests
diff --git a/addons/crm_iap_lead_enrich/__manifest__.py b/addons/crm_iap_lead_enrich/__manifest__.py
new file mode 100644
index 00000000000..c0a8e8e6f45
--- /dev/null
+++ b/addons/crm_iap_lead_enrich/__manifest__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'Lead Enrichment',
+ 'summary': 'Enrich Leads/Opportunities using email address domain',
+ 'category': 'Sales/CRM',
+ 'depends': [
+ 'iap',
+ 'crm',
+ ],
+ 'data': [
+ 'data/ir_cron.xml',
+ 'data/mail_data.xml',
+ 'views/res_config_settings_views.xml',
+ ],
+}
diff --git a/addons/crm_iap_lead_enrich/data/ir_cron.xml b/addons/crm_iap_lead_enrich/data/ir_cron.xml
new file mode 100644
index 00000000000..8817afd4ade
--- /dev/null
+++ b/addons/crm_iap_lead_enrich/data/ir_cron.xml
@@ -0,0 +1,15 @@
+
+
+
+ CRM: enrich leads (IAP)
+
+
+ code
+ model._iap_enrich_leads_cron()
+ 1
+ hours
+ -1
+
+
+
+
diff --git a/addons/crm_iap_lead_enrich/data/mail_data.xml b/addons/crm_iap_lead_enrich/data/mail_data.xml
new file mode 100644
index 00000000000..cc9a5a6b81a
--- /dev/null
+++ b/addons/crm_iap_lead_enrich/data/mail_data.xml
@@ -0,0 +1,116 @@
+
+
+
+
+ CRM: IAP Lead Enrichment No Credit
+ IAP Lead Enrichment Notification
+
+ iap@odoo.com
+ ${object.create_uid.email}
+
+
+
Dear ${object.create_uid.name or 'Madam/Sir'},
+
Unfortunately, there are no more credits on your IAP Lead Enrichment account.
+ You can charge it back from the Settings of the CRM app or from your IAP portal.
+
Best regards,
+
Odoo S.A.
+
+
+ ${object.create_uid.lang}
+
+
+
+
+ Lead Enrichment based on email address
+
+ No company data found based on the email address or email address is one of an email provider. No credit was consumed.
+
+
+
+
+ Lead Enrichment based on email address
+
+ Enrichment could not be done as no email address was provided.
+
+
+
+
+ Lead enriched based on email address
+
+
Your balance for Lead Enrichment is insufficient. Please go to your iap account to buy credits.
+
+
+
+
+
+ Lead enriched based on email address
+
+
+
+
diff --git a/addons/crm_iap_lead_enrich/models/__init__.py b/addons/crm_iap_lead_enrich/models/__init__.py
new file mode 100644
index 00000000000..0e5cee2fdbf
--- /dev/null
+++ b/addons/crm_iap_lead_enrich/models/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import crm_lead
+from . import iap_enrich_api
diff --git a/addons/crm_iap_lead_enrich/models/crm_lead.py b/addons/crm_iap_lead_enrich/models/crm_lead.py
new file mode 100644
index 00000000000..1c32078610c
--- /dev/null
+++ b/addons/crm_iap_lead_enrich/models/crm_lead.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import datetime
+import logging
+
+from odoo import api, fields, models, tools
+from odoo.addons.iap import InsufficientCreditError
+
+_logger = logging.getLogger(__name__)
+
+
+class Lead(models.Model):
+ _inherit = 'crm.lead'
+
+ reveal_id = fields.Char(string='Reveal ID', index=True)
+ iap_enrich_done = fields.Boolean(string='Enrichment done', help='Whether IAP service for lead enrichment based on email has been performed on this lead.')
+
+ @api.model
+ def _iap_enrich_leads_cron(self):
+ timeDelta = fields.datetime.now() - datetime.timedelta(hours=1)
+ leads = self.search([
+ ('iap_enrich_done', '=', False),
+ ('reveal_id', '=', False),
+ ('probability', 'not in', ('0', 100)),
+ ('create_date', '>', timeDelta)
+ ])
+ leads._iap_enrich(from_cron=True)
+
+ def _iap_enrich(self, from_cron=False):
+ lead_emails = {}
+ for lead in self:
+ if lead.probability in (0, 100) or lead.iap_enrich_done:
+ continue
+ normalized_email = tools.email_normalize(lead.partner_address_email) or tools.email_normalize(lead.email_from)
+ if normalized_email:
+ lead_emails[lead.id] = normalized_email.split('@')[1]
+ else:
+ lead.message_post_with_view(
+ 'crm_iap_lead_enrich.mail_message_lead_enrich_no_email',
+ subtype_id=self.env.ref('mail.mt_note').id)
+
+ if lead_emails:
+ try:
+ iap_response = self.env['iap.enrich.api']._request_enrich(lead_emails)
+ except InsufficientCreditError:
+ _logger.info('Sent batch %s enrich requests: failed because of credit', len(lead_emails))
+ self._iap_enrich_notify_no_more_credit()
+ if not from_cron:
+ data = {
+ 'url': self.env['iap.account'].get_credits_url('reveal'),
+ }
+ self[0].message_post_with_view(
+ 'crm_iap_lead_enrich.mail_message_lead_enrich_no_credit',
+ values=data,
+ subtype_id=self.env.ref('mail.mt_note').id)
+ except Exception as e:
+ _logger.info('Sent batch %s enrich requests: failed with exception %s', len(lead_emails), e)
+ else:
+ _logger.info('Sent batch %s enrich requests: success', len(lead_emails))
+ self._iap_enrich_from_response(iap_response)
+ self.env['ir.config_parameter'].sudo().set_param('crm_iap_lead_enrich.credit_notification', False)
+
+ @api.model
+ def _iap_enrich_from_response(self, iap_response):
+ """ Handle from the service and enrich the lead accordingly
+
+ :param iap_response: dict{lead_id: company data or False}
+ """
+ for lead in self.search([('id', 'in', list(iap_response.keys()))]): # handle unlinked data by performing a search
+ iap_data = iap_response.get(lead.id)
+ if not iap_data:
+ lead.message_post_with_view('crm_iap_lead_enrich.mail_message_lead_enrich_notfound', subtype_id=self.env.ref('mail.mt_note').id)
+ continue
+
+ values = {'iap_enrich_done': True}
+ lead_fields = ['description', 'partner_name', 'reveal_id', 'street', 'city', 'zip']
+ iap_fields = ['description', 'name', 'clearbit_id', 'location', 'city', 'city', 'postal_code']
+ for lead_field, iap_field in zip(lead_fields, iap_fields):
+ if not lead[lead_field] and iap_data.get(iap_field):
+ values[lead_field] = iap_data[iap_field]
+
+ if not lead.phone and iap_data.get('phone_numbers'):
+ values['phone'] = iap_data['phone_numbers'][0]
+ if not lead.mobile and iap_data.get('phone_numbers') and len(iap_data['phone_numbers']) > 1:
+ values['mobile'] = iap_data['phone_numbers'][1]
+ if not lead.country_id and iap_data.get('country_code'):
+ country = self.env['res.country'].search([('code', '=', iap_data['country_code'].upper())])
+ values['country_id'] = country.id
+ else:
+ country = lead.country_id
+ if not lead.state_id and country and iap_data.get('state_code'):
+ state = self.env['res.country.state'].search([
+ ('code', '=', iap_data['state_code']),
+ ('country_id', '=', country.id)
+ ])
+ values['state_id'] = state.id
+
+ lead.write(values)
+ lead.message_post_with_view(
+ 'crm_iap_lead_enrich.mail_message_lead_enrich_with_data',
+ values=lead._iap_enrich_get_message_data(iap_data),
+ subtype_id=self.env.ref('mail.mt_note').id
+ )
+
+ def _iap_enrich_get_message_data(self, company_data):
+ log_data = {
+ 'twitter': company_data.get('twitter'),
+ 'logo': company_data.get('logo'),
+ 'phone_numbers': company_data.get('phone_numbers'),
+ 'facebook': company_data.get('facebook'),
+ 'linkedin': company_data.get('linkedin'),
+ 'crunchbase': company_data.get('crunchbase'),
+ 'tech': [t.replace('_', ' ').title() for t in company_data.get('tech', [])],
+ }
+ timezone = company_data.get('timezone')
+ if timezone:
+ log_data.update({
+ 'timezone': timezone.replace('_', ' ').title(),
+ 'timezone_url': company_data.get('timezone_url'),
+ })
+ return log_data
+
+ @api.model
+ def _iap_enrich_notify_no_more_credit(self):
+ """ Notify when user has no credits anymore. In order to avoid to spam
+ people each hour, an ir.config_parameter is set. """
+ already_notified = self.env['ir.config_parameter'].sudo().get_param('crm_iap_lead_enrich.credit_notification')
+ if already_notified == 'True':
+ return
+
+ iap_account = self.env['iap.account'].search([('service_name', '=', 'reveal')], limit=1)
+ if not iap_account:
+ return
+
+ mail_template = self.env.ref('crm_iap_lead_enrich.mail_template_data_iap_lead_enrich_nocredit')
+ if not mail_template:
+ return
+
+ mail_template.sudo().send_mail(iap_account.id, force_send=False, notif_layout='mail.mail_notification_light')
+ self.env['ir.config_parameter'].sudo().set_param('lead_enrich.already_notified', True)
diff --git a/addons/crm_iap_lead_enrich/models/iap_enrich_api.py b/addons/crm_iap_lead_enrich/models/iap_enrich_api.py
new file mode 100644
index 00000000000..11ff8922404
--- /dev/null
+++ b/addons/crm_iap_lead_enrich/models/iap_enrich_api.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, api
+from odoo.addons import iap
+
+
+class IapEnrichAPI(models.AbstractModel):
+ _name = 'iap.enrich.api'
+ _description = 'IAP Lead Enrichment API'
+ _DEFAULT_ENDPOINT = 'https://iap-services.odoo.com'
+
+ @api.model
+ def _contact_iap(self, local_endpoint, params):
+ account = self.env['iap.account'].get('reveal')
+ dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
+ params['account_token'] = account.account_token
+ params['dbuuid'] = dbuuid
+ return iap.jsonrpc(self._DEFAULT_ENDPOINT + local_endpoint, params=params, timeout=300)
+
+ @api.model
+ def _request_enrich(self, lead_emails):
+ """ Contact endpoint to get enrichment data.
+
+ :param lead_emails: dict{lead_id: email}
+ :return: dict{lead_id: company data or False}
+ :raise: several errors, notably
+ * InsufficientCreditError: {
+ "credit": 4.0,
+ "service_name": "reveal",
+ "base_url": "https://iap.odoo.com/iap/1/credit",
+ "message": "You don't have enough credits on your account to use this service."
+ }
+ """
+ params = {
+ 'domains': lead_emails,
+ }
+ return self._contact_iap('/iap/clearbit/1/lead_enrichment_email', params=params)
diff --git a/addons/crm_iap_lead_enrich/tests/__init__.py b/addons/crm_iap_lead_enrich/tests/__init__.py
new file mode 100644
index 00000000000..19c6636d158
--- /dev/null
+++ b/addons/crm_iap_lead_enrich/tests/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import common
+from . import test_lead_enrich
diff --git a/addons/crm_iap_lead_enrich/tests/common.py b/addons/crm_iap_lead_enrich/tests/common.py
new file mode 100644
index 00000000000..7b9942a9a2c
--- /dev/null
+++ b/addons/crm_iap_lead_enrich/tests/common.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+
+from contextlib import contextmanager
+from functools import partial
+from unittest.mock import patch
+
+from odoo import exceptions
+from odoo.addons import iap
+from odoo.addons.crm_iap_lead_enrich.models.iap_enrich_api import IapEnrichAPI
+from odoo.tests import common, new_test_user
+
+crm_new_test_user = partial(new_test_user, context={'mail_create_nolog': True, 'mail_create_nosubscribe': True, 'mail_notrack': True, 'no_reset_password': True})
+
+
+class MockIAPEnrich(common.BaseCase):
+
+ @contextmanager
+ def mockIAPEnrichGateway(self, default_data=None, email_data=None, sim_error=None, failing_emails=None):
+
+ def _contact_iap(local_endpoint, params):
+ sim_result = {
+ 'name': 'Simulator INC',
+ 'location': 'Simulator Street',
+ 'city': 'SimCity',
+ 'postal_code': '9876',
+ 'country_code': 'BE',
+ 'clearbit_id': 'idontknow',
+ 'phone_numbers': ['+3269001122', '+32456001122'],
+ 'twitter': 'testtwitter',
+ 'facebook': 'testfacebook',
+ }
+ if default_data:
+ sim_result.update(default_data)
+ # mock single sms sending
+ if local_endpoint == '/iap/clearbit/1/lead_enrichment_email':
+ result = {}
+ for lead_id, email in params['domains'].items():
+ if sim_error and sim_error == 'credit':
+ raise iap.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
+ )
+ result[lead_id] = dict(sim_result)
+ if email_data and email_data.get(email):
+ result[lead_id].update(email_data[email])
+ return result
+
+ try:
+ with patch.object(IapEnrichAPI, '_contact_iap', side_effect=_contact_iap) as contact_iap_mock:
+ yield
+ finally:
+ pass
+
+
+class CrmCase(common.SavepointCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(CrmCase, cls).setUpClass()
+
+ cls.sales_manager = crm_new_test_user(
+ cls.env, login='sales_manager', groups='base.group_user,sales_team.group_sale_manager',
+ name='Martine SalesManager', email='"Martine SalesManager"
')
diff --git a/addons/crm_iap_lead_enrich/tests/test_lead_enrich.py b/addons/crm_iap_lead_enrich/tests/test_lead_enrich.py
new file mode 100644
index 00000000000..cc5ae025e24
--- /dev/null
+++ b/addons/crm_iap_lead_enrich/tests/test_lead_enrich.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.crm_iap_lead_enrich.tests import common as crm_iap_lead_enrich_common
+from odoo.tests.common import users
+
+
+class TestLeadEnrich(crm_iap_lead_enrich_common.CrmCase, crm_iap_lead_enrich_common.MockIAPEnrich):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestLeadEnrich, cls).setUpClass()
+
+ cls.leads = cls.env['crm.lead']
+ for x in range(0, 4):
+ cls.leads += cls.env['crm.lead'].create({
+ 'name': 'Test %s' % x,
+ 'email_from': 'test_mail_%s@example.com' % x
+ })
+
+ @users('sales_manager')
+ def test_enrich_internals(self):
+ leads = self.env['crm.lead'].browse(self.leads.ids)
+ leads[0].write({'partner_name': 'Already set', 'email_from': 'test@test1'})
+ leads.flush()
+ with self.mockIAPEnrichGateway(email_data={'test1': {'country_code': 'AU', 'state_code': 'NSW'}}):
+ leads._iap_enrich()
+
+ leads.flush()
+ self.assertEqual(leads[0].partner_name, 'Already set')
+ self.assertEqual(leads[0].country_id, self.env.ref('base.au'))
+ self.assertEqual(leads[0].state_id, self.env.ref('base.state_au_2'))
+ for lead in leads[1:]:
+ self.assertEqual(lead.partner_name, 'Simulator INC')
+ for lead in leads:
+ self.assertEqual(lead.street, 'Simulator Street')
+
+ # @users('sales_manager')
+ # def test_enrich_error_credit(self):
+ # leads = self.env['crm.lead'].browse(self.leads.ids)
+ # with self.mockIAPEnrichGateway(sim_error='credit'):
+ # leads._iap_enrich()
+
+ @users('sales_manager')
+ def test_enrich_error_jsonrpc_exception(self):
+ leads = self.env['crm.lead'].browse(self.leads.ids)
+ with self.mockIAPEnrichGateway(sim_error='jsonrpc_exception'):
+ leads._iap_enrich()
+
+ for lead in leads:
+ self.assertEqual(lead.street, False)
diff --git a/addons/crm_iap_lead_enrich/views/res_config_settings_views.xml b/addons/crm_iap_lead_enrich/views/res_config_settings_views.xml
new file mode 100644
index 00000000000..c637db82e07
--- /dev/null
+++ b/addons/crm_iap_lead_enrich/views/res_config_settings_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ res.config.settings.view.form.inherit.crm.iap.lead.enrich
+ res.config.settings
+
+
+
+
+
+
+
+
diff --git a/addons/iap/models/iap.py b/addons/iap/models/iap.py
index f712d2ae850..4279a2a908d 100644
--- a/addons/iap/models/iap.py
+++ b/addons/iap/models/iap.py
@@ -46,7 +46,6 @@ def jsonrpc(url, method='call', params=None, timeout=15):
'id': uuid.uuid4().hex,
}
-
_logger.info('iap jsonrpc %s', url)
try:
req = requests.post(url, json=payload, timeout=timeout)
diff --git a/addons/iap/static/src/js/crash_manager.js b/addons/iap/static/src/js/crash_manager.js
index c1d01e2cd2a..6ec0647b625 100644
--- a/addons/iap/static/src/js/crash_manager.js
+++ b/addons/iap/static/src/js/crash_manager.js
@@ -20,7 +20,7 @@ CrashManager.include({
*/
_getButtonMessage: function (isTrial){
var isEnterprise = _.last(odoo.session_info.server_version_info) === 'e';
- return isTrial && isEnterprise ? _t('Start a Trial at Odoo') : _t('Buy credits at Odoo');
+ return isTrial && isEnterprise ? _t('Start a Trial at Odoo') : _t('Buy credits');
},
/**
* @override