From 1a36d98e5f9791ea5b18a8249b9f3764437d7147 Mon Sep 17 00:00:00 2001 From: fleodoo Date: Tue, 28 May 2019 07:35:23 +0000 Subject: [PATCH] [ADD] crm_iap_lead_enrich: enrich leads based on email address PURPOSE Allow the user to enrich a lead based on the domain of the email address of the client. SPECIFICATIONS Following fields will be populated if found using clearbit and if they are not yet populated : * description * partner_name * reveal_id * website * street, street2, zip, city, country_id, state_id * phone (using the first number found in clearbit) * mobile (using the second number found in clearbit, else the same as phone) A message will also be logged in the chatter. If no data is found using clearbit no credit is used and a message will be logged. If the user has no credits he will be redirected to buy credits for the service. It is possible to enrich using the 'enrich' action in listview or with a cron that will run every hour on leads not older than 1 hour. Add a "Lead Enrichment" setting in CRM installing lead enrichment. LINKS Task 193185 closes odoo/odoo#36419 Signed-off-by: Thibault Delavallee (tde) --- addons/crm/models/res_config_settings.py | 1 + .../crm/views/res_config_settings_views.xml | 14 ++ addons/crm_iap_lead/static/img/crunchbase.ico | Bin 0 -> 9732 bytes addons/crm_iap_lead_enrich/__init__.py | 5 + addons/crm_iap_lead_enrich/__manifest__.py | 17 +++ addons/crm_iap_lead_enrich/data/ir_cron.xml | 15 ++ addons/crm_iap_lead_enrich/data/mail_data.xml | 116 ++++++++++++++ addons/crm_iap_lead_enrich/models/__init__.py | 5 + addons/crm_iap_lead_enrich/models/crm_lead.py | 141 ++++++++++++++++++ .../models/iap_enrich_api.py | 38 +++++ addons/crm_iap_lead_enrich/tests/__init__.py | 5 + addons/crm_iap_lead_enrich/tests/common.py | 64 ++++++++ .../tests/test_lead_enrich.py | 51 +++++++ .../views/res_config_settings_views.xml | 13 ++ addons/iap/models/iap.py | 1 - addons/iap/static/src/js/crash_manager.js | 2 +- 16 files changed, 486 insertions(+), 2 deletions(-) create mode 100644 addons/crm_iap_lead/static/img/crunchbase.ico create mode 100644 addons/crm_iap_lead_enrich/__init__.py create mode 100644 addons/crm_iap_lead_enrich/__manifest__.py create mode 100644 addons/crm_iap_lead_enrich/data/ir_cron.xml create mode 100644 addons/crm_iap_lead_enrich/data/mail_data.xml create mode 100644 addons/crm_iap_lead_enrich/models/__init__.py create mode 100644 addons/crm_iap_lead_enrich/models/crm_lead.py create mode 100644 addons/crm_iap_lead_enrich/models/iap_enrich_api.py create mode 100644 addons/crm_iap_lead_enrich/tests/__init__.py create mode 100644 addons/crm_iap_lead_enrich/tests/common.py create mode 100644 addons/crm_iap_lead_enrich/tests/test_lead_enrich.py create mode 100644 addons/crm_iap_lead_enrich/views/res_config_settings_views.xml diff --git a/addons/crm/models/res_config_settings.py b/addons/crm/models/res_config_settings.py index ca92e024dee..deb4fc322a9 100644 --- a/addons/crm/models/res_config_settings.py +++ b/addons/crm/models/res_config_settings.py @@ -13,6 +13,7 @@ class ResConfigSettings(models.TransientModel): group_use_lead = fields.Boolean(string="Leads", implied_group='crm.group_use_lead') module_crm_iap_lead = fields.Boolean("Generate new leads based on their country, industries, size, etc.") module_crm_iap_lead_website = fields.Boolean("Create Leads/Opportunities from your website's traffic") + module_crm_iap_lead_enrich = fields.Boolean("Enrich your leads automatically with company data based on their email address.") lead_mining_in_pipeline = fields.Boolean("Create a lead mining request directly from the opportunity pipeline.", config_parameter='crm.lead_mining_in_pipeline') predictive_lead_scoring_start_date = fields.Date(string='Lead Scoring Starting Date', compute="_compute_pls_start_date", inverse="_inverse_pls_start_date_str") predictive_lead_scoring_start_date_str = fields.Char(string='Lead Scoring Starting Date in String', default=date.today().strftime('%Y-%m-%d'), config_parameter='crm.pls_start_date') diff --git a/addons/crm/views/res_config_settings_views.xml b/addons/crm/views/res_config_settings_views.xml index 9b2f4938011..baa8dc3d925 100644 --- a/addons/crm/views/res_config_settings_views.xml +++ b/addons/crm/views/res_config_settings_views.xml @@ -85,6 +85,20 @@ +
+
+ +
+ +
+
+
+ +
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 0000000000000000000000000000000000000000..aa710c85b90e7517b2a4f3dcadebaf15cb8dd7fa GIT binary patch literal 9732 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 literal 0 HcmV?d00001 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} +
+ + + + + + + + + +
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