From fa26aaaa07d10eb2d579133a5ce6596a2f671139 Mon Sep 17 00:00:00 2001 From: nounoubensebia Date: Fri, 19 Mar 2021 10:32:14 +0000 Subject: [PATCH 1/4] [MOV] mail_client_extension: split authentication and main controller code This commit splits the code located in the mail_client_extension module into two separate files, an "authentication.py" file containing authentication related code and a "mail_client_extension.py" file containing everything else. Purpose is to better organize the code and to avoid having a bloated main controller. This commit does not change anything functionally, it only moves and refactors code. Task-2382870 UPG-PR: https://github.com/odoo/upgrade/pull/2156 COM-PR: https://github.com/odoo/odoo/pull/66139 --- .../controllers/__init__.py | 7 +- .../controllers/authenticate.py | 94 +++++++++++++++++++ .../controllers/{main.py => mail_plugin.py} | 79 +--------------- 3 files changed, 101 insertions(+), 79 deletions(-) create mode 100644 addons/mail_client_extension/controllers/authenticate.py rename addons/mail_client_extension/controllers/{main.py => mail_plugin.py} (67%) diff --git a/addons/mail_client_extension/controllers/__init__.py b/addons/mail_client_extension/controllers/__init__.py index f7273e63342..2de4f0eb2d5 100644 --- a/addons/mail_client_extension/controllers/__init__.py +++ b/addons/mail_client_extension/controllers/__init__.py @@ -1,2 +1,5 @@ -# -*- coding: utf-8 -* -from . import main \ No newline at end of file +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import authenticate +from . import mail_plugin diff --git a/addons/mail_client_extension/controllers/authenticate.py b/addons/mail_client_extension/controllers/authenticate.py new file mode 100644 index 00000000000..548ac66f11f --- /dev/null +++ b/addons/mail_client_extension/controllers/authenticate.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import datetime +import hmac +import json +import logging +import odoo +import werkzeug + +from odoo import http +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class Authenticate(http.Controller): + + @http.route('/mail_client_extension/auth', type='http', auth="user", methods=['GET'], website=True) + def auth(self, **values): + """ + Once authenticated this route renders the view that shows an app wants to access Odoo. + The user is invited to allow or deny the app. The form posts to `/mail_client_extension/auth/confirm`. + """ + return request.render('mail_client_extension.app_auth', values) + + @http.route('/mail_client_extension/auth/confirm', type='http', auth="user", methods=['POST']) + def auth_confirm(self, scope, friendlyname, redirect, info=None, do=None, **kw): + """ + Called by the `app_auth` template. If the user decided to allow the app to access Odoo, a temporary auth code + is generated and he is redirected to `redirect` with this code in the URL. It should redirect to the app, and + the app should then exchange this auth code for an access token by calling + `/mail_client_extension/auth/access_token`. + """ + parsed_redirect = werkzeug.urls.url_parse(redirect) + params = parsed_redirect.decode_query() + if do: + name = friendlyname if not info else f'{friendlyname}: {info}' + auth_code = self._generate_auth_code(scope, name) + # params is a MultiDict which does not support .update() with kwargs + params.update({'success': 1, 'auth_code': auth_code}) + else: + params['success'] = 0 + updated_redirect = parsed_redirect.replace(query=werkzeug.urls.url_encode(params)) + return werkzeug.utils.redirect(updated_redirect.to_url()) + + # In this case, an exception will be thrown in case of preflight request if only POST is allowed. + @http.route('/mail_client_extension/auth/access_token', type='json', auth="none", cors="*", + methods=['POST', 'OPTIONS']) + def auth_access_token(self, auth_code, **kw): + """ + Called by the external app to exchange an auth code, which is temporary and was passed in a URL, for an + access token, which is permanent, and can be used in the `Authorization` header to authorize subsequent requests + """ + auth_message = self._get_auth_code_data(auth_code) + if not auth_message: + return {"error": "Invalid code"} + request.uid = auth_message['uid'] + scope = 'odoo.plugin.' + auth_message.get('scope', '') + api_key = request.env['res.users.apikeys']._generate(scope, auth_message['name']) + return {'access_token': api_key} + + def _get_auth_code_data(self, auth_code): + data, auth_code_signature = auth_code.split('.') + data = base64.b64decode(data) + auth_code_signature = base64.b64decode(auth_code_signature) + signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_client_extension', data).encode() + if not hmac.compare_digest(auth_code_signature, signature): + return None + + auth_message = json.loads(data) + # Check the expiration + if datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(auth_message['timestamp']) > datetime.timedelta( + minutes=3): + return None + + return auth_message + + # Using UTC explicitly in case of a distributed system where the generation and the signature verification do not + # necessarily happen on the same server + def _generate_auth_code(self, scope, name): + auth_dict = { + 'scope': scope, + 'name': name, + 'timestamp': int(datetime.datetime.utcnow().timestamp()), + # <- elapsed time should be < 3 mins when verifying + 'uid': request.uid, + } + auth_message = json.dumps(auth_dict, sort_keys=True).encode() + signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_client_extension', auth_message).encode() + auth_code = "%s.%s" % (base64.b64encode(auth_message).decode(), base64.b64encode(signature).decode()) + _logger.info('Auth code created - user %s, scope %s', request.env.user, scope) + return auth_code diff --git a/addons/mail_client_extension/controllers/main.py b/addons/mail_client_extension/controllers/mail_plugin.py similarity index 67% rename from addons/mail_client_extension/controllers/main.py rename to addons/mail_client_extension/controllers/mail_plugin.py index 42f2687b1f1..f6bd179755c 100644 --- a/addons/mail_client_extension/controllers/main.py +++ b/addons/mail_client_extension/controllers/mail_plugin.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + import base64 -import datetime -import hmac import json import logging -import odoo import requests import werkzeug @@ -17,80 +15,7 @@ from odoo.tools.misc import formatLang _logger = logging.getLogger(__name__) -class MailClientExtensionController(http.Controller): - - @http.route('/mail_client_extension/auth', type='http', auth="user", methods=['GET'], website=True) - def auth(self, **values): - """ - Once authenticated this route renders the view that shows an app wants to access Odoo. - The user is invited to allow or deny the app. The form posts to `/mail_client_extension/auth/confirm`. - """ - return request.render('mail_client_extension.app_auth', values) - - @http.route('/mail_client_extension/auth/confirm', type='http', auth="user", methods=['POST']) - def auth_confirm(self, scope, friendlyname, redirect, info=None, do=None, **kw): - """ - Called by the `app_auth` template. If the user decided to allow the app to access Odoo, a temporary auth code - is generated and he is redirected to `redirect` with this code in the URL. It should redirect to the app, and - the app should then exchange this auth code for an access token by calling - `/mail_client_extension/auth/access_token`. - """ - parsed_redirect = werkzeug.urls.url_parse(redirect) - params = parsed_redirect.decode_query() - if do: - name = friendlyname if not info else f'{friendlyname}: {info}' - auth_code = self._generate_auth_code(scope, name) - # params is a MultiDict which does not support .update() with kwargs - params.update({'success': 1, 'auth_code': auth_code}) - else: - params['success'] = 0 - updated_redirect = parsed_redirect.replace(query=werkzeug.urls.url_encode(params)) - return werkzeug.utils.redirect(updated_redirect.to_url()) - - # In this case, an exception will be thrown in case of preflight request if only POST is allowed. - @http.route('/mail_client_extension/auth/access_token', type='json', auth="none", cors="*", methods=['POST', 'OPTIONS']) - def auth_access_token(self, auth_code, **kw): - """ - Called by the external app to exchange an auth code, which is temporary and was passed in a URL, for an - access token, which is permanent, and can be used in the `Authorization` header to authorize subsequent requests - """ - auth_message = self._get_auth_code_data(auth_code) - if not auth_message: - return {"error": "Invalid code"} - request.uid = auth_message['uid'] - scope = 'odoo.plugin.' + auth_message.get('scope', '') - api_key = request.env['res.users.apikeys']._generate(scope, auth_message['name']) - return {'access_token': api_key } - - def _get_auth_code_data(self, auth_code): - data, auth_code_signature = auth_code.split('.') - data = base64.b64decode(data) - auth_code_signature = base64.b64decode(auth_code_signature) - signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_client_extension', data).encode() - if not hmac.compare_digest(auth_code_signature, signature): - return None - - auth_message = json.loads(data) - # Check the expiration - if datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(auth_message['timestamp']) > datetime.timedelta(minutes=3): - return None - - return auth_message - - # Using UTC explicitly in case of a distributed system where the generation and the signature verification do not - # necessarily happen on the same server - def _generate_auth_code(self, scope, name): - auth_dict = { - 'scope': scope, - 'name': name, - 'timestamp': int(datetime.datetime.utcnow().timestamp()), # <- elapsed time should be < 3 mins when verifying - 'uid': request.uid, - } - auth_message = json.dumps(auth_dict, sort_keys=True).encode() - signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_client_extension', auth_message).encode() - auth_code = "%s.%s" % (base64.b64encode(auth_message).decode(), base64.b64encode(signature).decode()) - _logger.info('Auth code created - user %s, scope %s', request.env.user, scope) - return auth_code +class MailPluginController(http.Controller): def _iap_enrich(self, domain): enriched_data = {} From 6c81ee556fa7a8037769ef2c906a820931e28e97 Mon Sep 17 00:00:00 2001 From: nounoubensebia Date: Thu, 11 Feb 2021 11:17:17 +0000 Subject: [PATCH 2/4] [REF] [crm_]mail_client_extension, base_setup, crm[_iap_lead_enrich], iap: split mail specific and crm specific code - split the mail_client_extension module onto two modules: 1) module mail_client_extension having mail/contact specific code, depends on iap and contacts only 2) module crm_mail_client_extension having crm specific code, depends on CRM and mail_client_extension. Purpose is to allow the user to use the mail_client_extension without needing to install CRM, this will also allow the usage of the mail connector in other modules without the need for CRM. - add the module_mail_client_extension field to the res_config_settings model of the base_setup module in order to enable the user to install the mail_client_extension module directly from the settings - move the crm_lead model from the mail_client_extension_module to the newly created crm_mail_client_extension_module, as this model is no longer needed by the mail_client_extension module because it no longer not implemants CRM functionality - move the iap_enrich_api model from the crm_iap_lead_enrich module onto the iap module as it is only related to contacts and not to CRM, this way it can be used in other modules such as the mail_client_extension module without the need for CRM module Task-2382870 UPG-PR: https://github.com/odoo/upgrade/pull/2156 COM-PR: https://github.com/odoo/odoo/pull/66139 --- .../base_setup/models/res_config_settings.py | 3 ++ .../views/res_config_settings_views.xml | 26 +++++++++---- addons/crm/models/res_config_settings.py | 1 - .../crm/views/res_config_settings_views.xml | 13 ------- addons/crm_iap_lead_enrich/models/__init__.py | 1 - addons/crm_iap_lead_enrich/tests/__init__.py | 1 - .../tests/test_lead_enrich.py | 2 +- addons/crm_mail_client_extension/__init__.py | 5 +++ .../crm_mail_client_extension/__manifest__.py | 22 +++++++++++ .../controllers/__init__.py | 4 ++ .../controllers/mail_plugin.py | 37 +++++++++++++++++++ .../models/__init__.py | 4 ++ .../models/crm_lead.py | 0 .../views/crm_mail_client_extension_lead.xml} | 0 addons/iap/models/__init__.py | 1 + .../models/iap_enrich_api.py | 0 addons/iap/tests/__init__.py | 1 + .../tests/common.py | 2 +- addons/mail_client_extension/__init__.py | 5 ++- addons/mail_client_extension/__manifest__.py | 10 ++--- .../controllers/mail_plugin.py | 25 ------------- .../mail_client_extension/models/__init__.py | 6 ++- 22 files changed, 110 insertions(+), 59 deletions(-) create mode 100644 addons/crm_mail_client_extension/__init__.py create mode 100644 addons/crm_mail_client_extension/__manifest__.py create mode 100644 addons/crm_mail_client_extension/controllers/__init__.py create mode 100644 addons/crm_mail_client_extension/controllers/mail_plugin.py create mode 100644 addons/crm_mail_client_extension/models/__init__.py rename addons/{mail_client_extension => crm_mail_client_extension}/models/crm_lead.py (100%) rename addons/{mail_client_extension/views/mail_client_extension_lead.xml => crm_mail_client_extension/views/crm_mail_client_extension_lead.xml} (100%) rename addons/{crm_iap_lead_enrich => iap}/models/iap_enrich_api.py (100%) rename addons/{crm_iap_lead_enrich => iap}/tests/common.py (96%) diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py index f1fd84be745..ff6dc35bc3c 100644 --- a/addons/base_setup/models/res_config_settings.py +++ b/addons/base_setup/models/res_config_settings.py @@ -22,6 +22,9 @@ class ResConfigSettings(models.TransientModel): string='Allow the users to synchronize their calendar with Google Calendar') module_microsoft_calendar = fields.Boolean( string='Allow the users to synchronize their calendar with Outlook Calendar') + module_mail_client_extension = fields.Boolean( + string='Allow integration with the mail plugins' + ) module_google_drive = fields.Boolean("Attach Google documents to any record") module_google_spreadsheet = fields.Boolean("Google Spreadsheet") module_auth_oauth = fields.Boolean("Use external authentication providers (OAuth)") diff --git a/addons/base_setup/views/res_config_settings_views.xml b/addons/base_setup/views/res_config_settings_views.xml index 1063f056842..ecc361dc8fd 100644 --- a/addons/base_setup/views/res_config_settings_views.xml +++ b/addons/base_setup/views/res_config_settings_views.xml @@ -210,17 +210,15 @@

Integrations

-
+
- +
-
@@ -239,6 +237,20 @@
+
+
+ +
+
+
+
diff --git a/addons/crm/models/res_config_settings.py b/addons/crm/models/res_config_settings.py index f9165f22188..70e3ca98420 100644 --- a/addons/crm/models/res_config_settings.py +++ b/addons/crm/models/res_config_settings.py @@ -39,7 +39,6 @@ class ResConfigSettings(models.TransientModel): 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.") - module_mail_client_extension = fields.Boolean("See and manage users, companies, and leads from our mail client extensions.") lead_enrich_auto = fields.Selection([ ('manual', 'Enrich leads on demand only'), ('auto', 'Enrich all leads automatically'), diff --git a/addons/crm/views/res_config_settings_views.xml b/addons/crm/views/res_config_settings_views.xml index 5bd0de10945..e3623f7a5c4 100644 --- a/addons/crm/views/res_config_settings_views.xml +++ b/addons/crm/views/res_config_settings_views.xml @@ -119,19 +119,6 @@

Lead Generation

-
-
- -
- -
-
-
diff --git a/addons/crm_iap_lead_enrich/models/__init__.py b/addons/crm_iap_lead_enrich/models/__init__.py index dcb5aa3d8a4..7ac0406ee23 100644 --- a/addons/crm_iap_lead_enrich/models/__init__.py +++ b/addons/crm_iap_lead_enrich/models/__init__.py @@ -2,5 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import crm_lead -from . import iap_enrich_api from . import res_config_settings diff --git a/addons/crm_iap_lead_enrich/tests/__init__.py b/addons/crm_iap_lead_enrich/tests/__init__.py index 19c6636d158..33ab21b0da9 100644 --- a/addons/crm_iap_lead_enrich/tests/__init__.py +++ b/addons/crm_iap_lead_enrich/tests/__init__.py @@ -1,5 +1,4 @@ # -*- 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/test_lead_enrich.py b/addons/crm_iap_lead_enrich/tests/test_lead_enrich.py index 9d90189322c..9b7efb0f895 100644 --- a/addons/crm_iap_lead_enrich/tests/test_lead_enrich.py +++ b/addons/crm_iap_lead_enrich/tests/test_lead_enrich.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.addons.crm.tests.common import TestCrmCommon -from odoo.addons.crm_iap_lead_enrich.tests.common import MockIAPEnrich +from odoo.addons.iap.tests.common import MockIAPEnrich from odoo.tests.common import users diff --git a/addons/crm_mail_client_extension/__init__.py b/addons/crm_mail_client_extension/__init__.py new file mode 100644 index 00000000000..8c67f893b37 --- /dev/null +++ b/addons/crm_mail_client_extension/__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 controllers diff --git a/addons/crm_mail_client_extension/__manifest__.py b/addons/crm_mail_client_extension/__manifest__.py new file mode 100644 index 00000000000..8cc78874c2d --- /dev/null +++ b/addons/crm_mail_client_extension/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'CRM Mail Client Extension', + 'version': '1.0', + 'category': 'Sales/CRM', + 'sequence': 5, + 'summary': 'Turn emails received in your mailbox into leads and log their content as internal notes.', + 'description': "Turn emails received in your mailbox into leads and log their content as internal notes.", + 'website': 'https://www.odoo.com/page/crm', + 'depends': [ + 'crm', + 'mail_client_extension', + ], + 'data': [ + 'views/crm_mail_client_extension_lead.xml' + ], + 'installable': True, + 'application': False, + 'auto_install': True +} diff --git a/addons/crm_mail_client_extension/controllers/__init__.py b/addons/crm_mail_client_extension/controllers/__init__.py new file mode 100644 index 00000000000..01119c1c5a2 --- /dev/null +++ b/addons/crm_mail_client_extension/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import mail_plugin diff --git a/addons/crm_mail_client_extension/controllers/mail_plugin.py b/addons/crm_mail_client_extension/controllers/mail_plugin.py new file mode 100644 index 00000000000..df21e0c7a7d --- /dev/null +++ b/addons/crm_mail_client_extension/controllers/mail_plugin.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import werkzeug + +from odoo import http +from odoo.http import request +from odoo.tools.misc import formatLang + +_logger = logging.getLogger(__name__) + + +class MailPluginController(http.Controller): + + @http.route('/mail_client_extension/log_single_mail_content', type="json", auth="outlook", cors="*") + def log_single_mail_content(self, lead, message, **kw): + crm_lead = request.env['crm.lead'].browse(lead) + crm_lead.message_post(body=message) + + @http.route('/mail_client_extension/lead/get_by_partner_id', type="json", auth="outlook", cors="*") + def crm_lead_get_by_partner_id(self, partner, limit, offset, **kwargs): + partner_leads = request.env['crm.lead'].search([('partner_id', '=', partner)], offset=offset, limit=limit) + leads = [] + for lead in partner_leads: + leads.append({ + 'id': lead.id, + 'name': lead.name, + 'expected_revenue': formatLang(request.env, lead.expected_revenue, monetary=True, currency_obj=lead.company_currency), + }) + + return {'leads': leads} + + @http.route('/mail_client_extension/lead/create_from_partner', type='http', auth='user', methods=['GET']) + def crm_lead_redirect_form_view(self, partner_id): + server_action = http.request.env.ref("crm_mail_client_extension.lead_creation_prefilled_action") + return werkzeug.utils.redirect('/web#action=%s&model=crm.lead&partner_id=%s' % (server_action.id, int(partner_id))) diff --git a/addons/crm_mail_client_extension/models/__init__.py b/addons/crm_mail_client_extension/models/__init__.py new file mode 100644 index 00000000000..00140f99c9f --- /dev/null +++ b/addons/crm_mail_client_extension/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import crm_lead diff --git a/addons/mail_client_extension/models/crm_lead.py b/addons/crm_mail_client_extension/models/crm_lead.py similarity index 100% rename from addons/mail_client_extension/models/crm_lead.py rename to addons/crm_mail_client_extension/models/crm_lead.py diff --git a/addons/mail_client_extension/views/mail_client_extension_lead.xml b/addons/crm_mail_client_extension/views/crm_mail_client_extension_lead.xml similarity index 100% rename from addons/mail_client_extension/views/mail_client_extension_lead.xml rename to addons/crm_mail_client_extension/views/crm_mail_client_extension_lead.xml diff --git a/addons/iap/models/__init__.py b/addons/iap/models/__init__.py index d148377834f..2e95fd52ee5 100644 --- a/addons/iap/models/__init__.py +++ b/addons/iap/models/__init__.py @@ -3,3 +3,4 @@ from . import iap_account from . import res_config_settings +from . import iap_enrich_api diff --git a/addons/crm_iap_lead_enrich/models/iap_enrich_api.py b/addons/iap/models/iap_enrich_api.py similarity index 100% rename from addons/crm_iap_lead_enrich/models/iap_enrich_api.py rename to addons/iap/models/iap_enrich_api.py diff --git a/addons/iap/tests/__init__.py b/addons/iap/tests/__init__.py index 3c032a13d8b..4a6cacf0f89 100644 --- a/addons/iap/tests/__init__.py +++ b/addons/iap/tests/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_iap +from . import common diff --git a/addons/crm_iap_lead_enrich/tests/common.py b/addons/iap/tests/common.py similarity index 96% rename from addons/crm_iap_lead_enrich/tests/common.py rename to addons/iap/tests/common.py index fb4b40e4269..1abed724698 100644 --- a/addons/crm_iap_lead_enrich/tests/common.py +++ b/addons/iap/tests/common.py @@ -6,7 +6,7 @@ from unittest.mock import patch from odoo import exceptions from odoo.addons.iap.tools import iap_tools -from odoo.addons.crm_iap_lead_enrich.models.iap_enrich_api import IapEnrichAPI +from odoo.addons.iap.models.iap_enrich_api import IapEnrichAPI from odoo.tests import common diff --git a/addons/mail_client_extension/__init__.py b/addons/mail_client_extension/__init__.py index 38718f0843b..8c67f893b37 100644 --- a/addons/mail_client_extension/__init__.py +++ b/addons/mail_client_extension/__init__.py @@ -1,2 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from . import models -from . import controllers \ No newline at end of file +from . import controllers diff --git a/addons/mail_client_extension/__manifest__.py b/addons/mail_client_extension/__manifest__.py index 9c2f8417118..62f19489363 100644 --- a/addons/mail_client_extension/__manifest__.py +++ b/addons/mail_client_extension/__manifest__.py @@ -6,17 +6,15 @@ 'version': '1.0', 'category': 'Sales/CRM', 'sequence': 5, - 'summary': 'Turn emails received in your Outlook mailbox into leads and log their content as internal notes.', - 'description': "Turn emails received in your Outlook mailbox into leads and log their content as internal notes.", - 'website': 'https://www.odoo.com/page/crm', + 'summary': 'Allows integration with mail plugins.', + 'description': "Integrate Odoo with your mailbox, get information about contacts directly inside your mailbox, log content of emails as internal notes", 'depends': [ 'web', - 'crm', - 'crm_iap_lead_enrich' + 'contacts', + 'iap' ], 'data': [ 'views/mail_client_extension_login.xml', - 'views/mail_client_extension_lead.xml', ], 'installable': True, 'application': False, diff --git a/addons/mail_client_extension/controllers/mail_plugin.py b/addons/mail_client_extension/controllers/mail_plugin.py index f6bd179755c..ff7184d0ef0 100644 --- a/addons/mail_client_extension/controllers/mail_plugin.py +++ b/addons/mail_client_extension/controllers/mail_plugin.py @@ -5,12 +5,10 @@ import base64 import json import logging import requests -import werkzeug from odoo import http, tools from odoo.addons.iap.tools import iap_tools from odoo.http import request -from odoo.tools.misc import formatLang _logger = logging.getLogger(__name__) @@ -175,26 +173,3 @@ class MailPluginController(http.Controller): response = {'id': partner.id} return response - - @http.route('/mail_client_extension/log_single_mail_content', type="json", auth="outlook", cors="*") - def log_single_mail_content(self, lead, message, **kw): - crm_lead = request.env['crm.lead'].browse(lead) - crm_lead.message_post(body=message) - - @http.route('/mail_client_extension/lead/get_by_partner_id', type="json", auth="outlook", cors="*") - def crm_lead_get_by_partner_id(self, partner, limit, offset, **kwargs): - partner_leads = request.env['crm.lead'].search([('partner_id', '=', partner)], offset=offset, limit=limit) - leads = [] - for lead in partner_leads: - leads.append({ - 'id': lead.id, - 'name': lead.name, - 'expected_revenue': formatLang(request.env, lead.expected_revenue, monetary=True, currency_obj=lead.company_currency), - }) - - return {'leads': leads} - - @http.route('/mail_client_extension/lead/create_from_partner', type='http', auth='user', methods=['GET']) - def crm_lead_redirect_form_view(self, partner_id): - server_action = http.request.env.ref("mail_client_extension.lead_creation_prefilled_action") - return werkzeug.utils.redirect('/web#action=%s&model=crm.lead&partner_id=%s' % (server_action.id, int(partner_id))) diff --git a/addons/mail_client_extension/models/__init__.py b/addons/mail_client_extension/models/__init__.py index 47f7bea7264..4d45ab6ee21 100644 --- a/addons/mail_client_extension/models/__init__.py +++ b/addons/mail_client_extension/models/__init__.py @@ -1,3 +1,5 @@ -from . import crm_lead +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import ir_http from . import res_partner -from . import ir_http \ No newline at end of file From 51070280822ab3d5cf0788f076885ac641cafbd3 Mon Sep 17 00:00:00 2001 From: nounoubensebia Date: Tue, 16 Feb 2021 08:19:58 +0000 Subject: [PATCH 3/4] [IMP] [crm_]mail_client_extension: add contact & multi-record support This commit adds support for searching multiple contacts using a text query in the email plugin rather then forcing the user to stick with a single matching contact. This is done to give more functionnality to the user and to enable him to perform actions (log emails, create opportunities...) on other contacts and not only on the matched contact. It also brings the following changes: - Enable the user to create a lead with the email message being directly set in the description field, as the user would probable want the email message to be included in the lead, this is done through the "crm_lead_create" method, which the plugins will use in order to create the lead and send the related information, the plugin would then directly redirect the user to the lead form view realted to the created lead in edit mode. - Extra information about the contact (leads, tickets...) are returned directly with the contact using the _prepare_contact_values method which other modules could override by adding new keys in the returned dictionary to provide more data, this is done in order to eliminate the need for the get_modules method and also to avoid adding new routes in the sub modules, it also eases the future maintenance of the plugins as we can freely add new data without breaking older versions of the plugins - Adds the ability to log emails into contacts, and refactors the logging method to make it able to log emails into all "loggable" models, these models are returned by the mail_content_logging_models_whitelist method which can also be overriden in sub modules to add new loggable models, Thus eliminating the need for creating new routes/methods in sub modules if we want to log emails into new models - In case the partner is present in the database, no longer implicitly enrich and link a company to the partner, as this can lead to some undesired behaviour (the partner will for example have his related invoices with the newly linked company header), the user would still be able to do this but only if he explicitly clicks on the enrich button, for this reason the new "res_partner_enrich_and_create_company" route has been added in order to enrich a partner if the user wants to. - Add a state to Odoo to know which script Gmail should execute when the user is redirected to Gmail. - change route names from mail_client_extension/... to mail_plugin/... because the old mail_client_extension makes route names too long and is too verbose. Legacy and deprecated routes/methods/actions have not been deleted in order to ensure that older versions of the plugin would still work after this update, these are now marked with a comment. Task-2427737 UPG-PR: https://github.com/odoo/upgrade/pull/2156 COM-PR: https://github.com/odoo/odoo/pull/66139 --- .../crm_mail_client_extension/__manifest__.py | 3 +- .../controllers/__init__.py | 1 + .../controllers/crm_client.py | 67 ++++ .../controllers/mail_plugin.py | 64 ++-- .../models/crm_lead.py | 5 + .../views/crm_lead_views.xml | 16 + .../views/crm_mail_client_extension_lead.xml | 1 + .../controllers/authenticate.py | 22 +- .../controllers/mail_plugin.py | 300 ++++++++++++------ .../views/mail_client_extension_login.xml | 3 +- 10 files changed, 364 insertions(+), 118 deletions(-) create mode 100644 addons/crm_mail_client_extension/controllers/crm_client.py create mode 100644 addons/crm_mail_client_extension/views/crm_lead_views.xml diff --git a/addons/crm_mail_client_extension/__manifest__.py b/addons/crm_mail_client_extension/__manifest__.py index 8cc78874c2d..b3f2a016cf8 100644 --- a/addons/crm_mail_client_extension/__manifest__.py +++ b/addons/crm_mail_client_extension/__manifest__.py @@ -14,7 +14,8 @@ 'mail_client_extension', ], 'data': [ - 'views/crm_mail_client_extension_lead.xml' + 'views/crm_mail_client_extension_lead.xml', + 'views/crm_lead_views.xml' ], 'installable': True, 'application': False, diff --git a/addons/crm_mail_client_extension/controllers/__init__.py b/addons/crm_mail_client_extension/controllers/__init__.py index 01119c1c5a2..e7d2c7f26ec 100644 --- a/addons/crm_mail_client_extension/controllers/__init__.py +++ b/addons/crm_mail_client_extension/controllers/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import crm_client from . import mail_plugin diff --git a/addons/crm_mail_client_extension/controllers/crm_client.py b/addons/crm_mail_client_extension/controllers/crm_client.py new file mode 100644 index 00000000000..459f65100b8 --- /dev/null +++ b/addons/crm_mail_client_extension/controllers/crm_client.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import werkzeug + +from odoo import http +from odoo.http import request +from odoo.tools import html2plaintext + + +class CrmClient(http.Controller): + + @http.route(route='/mail_client_extension/log_single_mail_content', + type="json", auth="outlook", cors="*") + def log_single_mail_content(self, lead, message, **kw): + """ + deprecated as of saas-14.3, not needed for newer versions of the mail plugin but necessary + for supporting older versions + """ + crm_lead = request.env['crm.lead'].browse(lead) + crm_lead.message_post(body=message) + + @http.route('/mail_client_extension/lead/get_by_partner_id', type="json", auth="outlook", cors="*") + def crm_lead_get_by_partner_id(self, partner, limit=5, offset=0, **kwargs): + """ + deprecated as of saas-14.3, not needed for newer versions of the mail plugin but necessary + for supporting older versions + """ + partner_instance = request.env['res.partner'].browse(partner) + return {'leads': self._fetch_partner_leads(partner_instance, limit, offset)} + + @http.route('/mail_client_extension/lead/create_from_partner', type='http', auth='user', methods=['GET']) + def crm_lead_redirect_create_form_view(self, partner_id): + """ + deprecated as of saas-14.3, not needed for newer versions of the mail plugin but necessary + for supporting older versions + """ + server_action = http.request.env.ref("crm_mail_client_extension.lead_creation_prefilled_action") + return werkzeug.utils.redirect( + '/web#action=%s&model=crm.lead&partner_id=%s' % (server_action.id, int(partner_id))) + + @http.route('/mail_plugin/lead/create', type='json', auth='outlook', cors="*") + def crm_lead_create(self, partner_id, email_body, email_subject): + partner = request.env['res.partner'].browse(partner_id).exists() + if not partner: + return {'error': 'partner_not_found'} + + # In a multi-company environment, we will not be able to create a lead if the user is connected to a different + # company then the one linked to the contact, this is not supported, and users are encouraged to check if they + # are connected to the right company before performing such operation. + record = request.env['crm.lead'].create({ + 'name': html2plaintext(email_subject), + 'partner_id': partner_id, + 'description': html2plaintext(email_body), + }) + + return {'lead_id': record.id} + + @http.route('/mail_client_extension/lead/open', type='http', auth='user') + def crm_lead_open(self, lead_id): + """ + deprecated as of saas-14.3, not needed for newer versions of the mail plugin but necessary + for supporting older versions + """ + action = http.request.env.ref("crm.crm_lead_view_form") + url = '/web#id=%s&action=%s&model=crm.lead&edit=1&model=crm.lead' % (lead_id, action.id) + return werkzeug.utils.redirect(url) diff --git a/addons/crm_mail_client_extension/controllers/mail_plugin.py b/addons/crm_mail_client_extension/controllers/mail_plugin.py index df21e0c7a7d..c25ad893169 100644 --- a/addons/crm_mail_client_extension/controllers/mail_plugin.py +++ b/addons/crm_mail_client_extension/controllers/mail_plugin.py @@ -2,36 +2,62 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging -import werkzeug -from odoo import http from odoo.http import request from odoo.tools.misc import formatLang +from odoo.addons.mail_client_extension.controllers import mail_plugin + _logger = logging.getLogger(__name__) -class MailPluginController(http.Controller): +class MailPluginController(mail_plugin.MailPluginController): - @http.route('/mail_client_extension/log_single_mail_content', type="json", auth="outlook", cors="*") - def log_single_mail_content(self, lead, message, **kw): - crm_lead = request.env['crm.lead'].browse(lead) - crm_lead.message_post(body=message) + def _fetch_partner_leads(self, partner, limit=5, offset=0): + """ + Returns an array containing partner leads, each lead will have the following structure : + { + id: the lead's id, + name: the lead's name, + expected_revenue: the expected revenue field value + probability: the value of the probability field, + recurring_revenue: the value of the recurring_revenue field if the lead has a recurring revenue + recurring_plan: the value of the recurring plan field if the lead has a recurring revenue + } + """ + + partner_leads = request.env['crm.lead'].search( + [('partner_id', '=', partner.id)], offset=offset, limit=limit) + recurring_revenues = request.env.user.has_group('crm.group_use_recurring_revenues') - @http.route('/mail_client_extension/lead/get_by_partner_id', type="json", auth="outlook", cors="*") - def crm_lead_get_by_partner_id(self, partner, limit, offset, **kwargs): - partner_leads = request.env['crm.lead'].search([('partner_id', '=', partner)], offset=offset, limit=limit) leads = [] for lead in partner_leads: - leads.append({ - 'id': lead.id, + lead_values = { + 'lead_id': lead.id, 'name': lead.name, - 'expected_revenue': formatLang(request.env, lead.expected_revenue, monetary=True, currency_obj=lead.company_currency), - }) + 'expected_revenue': formatLang(request.env, lead.expected_revenue, monetary=True, + currency_obj=lead.company_currency), + 'probability': lead.probability, + } - return {'leads': leads} + if recurring_revenues: + lead_values.update({ + 'recurring_revenue': formatLang(request.env, lead.recurring_revenue, monetary=True, + currency_obj=lead.company_currency), + 'recurring_plan': lead.recurring_plan.name, + }) - @http.route('/mail_client_extension/lead/create_from_partner', type='http', auth='user', methods=['GET']) - def crm_lead_redirect_form_view(self, partner_id): - server_action = http.request.env.ref("crm_mail_client_extension.lead_creation_prefilled_action") - return werkzeug.utils.redirect('/web#action=%s&model=crm.lead&partner_id=%s' % (server_action.id, int(partner_id))) + leads.append(lead_values) + + return leads + + def _prepare_contact_values(self, partner): + contact_values = super(MailPluginController, self)._prepare_contact_values(partner) + if not partner: + contact_values['leads'] = [] + else: + contact_values['leads'] = self._fetch_partner_leads(partner) + return contact_values + + def _mail_content_logging_models_whitelist(self): + return super(MailPluginController, self)._mail_content_logging_models_whitelist() + ['crm.lead'] diff --git a/addons/crm_mail_client_extension/models/crm_lead.py b/addons/crm_mail_client_extension/models/crm_lead.py index 1bc5c8f0f3d..588504bf9ab 100644 --- a/addons/crm_mail_client_extension/models/crm_lead.py +++ b/addons/crm_mail_client_extension/models/crm_lead.py @@ -3,11 +3,16 @@ from odoo import api, models + class Lead(models.Model): _inherit = 'crm.lead' @api.model def _form_view_auto_fill(self): + """ + deprecated as of saas-14.3, not needed for newer versions of the mail plugin but necessary + for supporting older versions + """ return { 'type': 'ir.actions.act_window', 'view_mode': 'form', diff --git a/addons/crm_mail_client_extension/views/crm_lead_views.xml b/addons/crm_mail_client_extension/views/crm_lead_views.xml new file mode 100644 index 00000000000..3eb3bb2dca5 --- /dev/null +++ b/addons/crm_mail_client_extension/views/crm_lead_views.xml @@ -0,0 +1,16 @@ + + + + + Lead: redirect form in edit mode + ir.actions.act_window + crm.lead + form + + + { + 'form_view_initial_mode': 'edit', + } + + + diff --git a/addons/crm_mail_client_extension/views/crm_mail_client_extension_lead.xml b/addons/crm_mail_client_extension/views/crm_mail_client_extension_lead.xml index f13a110ec1a..c087782f7cd 100644 --- a/addons/crm_mail_client_extension/views/crm_mail_client_extension_lead.xml +++ b/addons/crm_mail_client_extension/views/crm_mail_client_extension_lead.xml @@ -1,5 +1,6 @@ + Redirection to the lead creation form with prefilled info ir.actions.server diff --git a/addons/mail_client_extension/controllers/authenticate.py b/addons/mail_client_extension/controllers/authenticate.py index 548ac66f11f..7c96423d12b 100644 --- a/addons/mail_client_extension/controllers/authenticate.py +++ b/addons/mail_client_extension/controllers/authenticate.py @@ -17,21 +17,27 @@ _logger = logging.getLogger(__name__) class Authenticate(http.Controller): - @http.route('/mail_client_extension/auth', type='http', auth="user", methods=['GET'], website=True) + @http.route(['/mail_client_extension/auth', '/mail_plugin/auth'], type='http', auth="user", methods=['GET'], website=True) def auth(self, **values): """ Once authenticated this route renders the view that shows an app wants to access Odoo. The user is invited to allow or deny the app. The form posts to `/mail_client_extension/auth/confirm`. + + old route name "/mail_client_extension/auth is deprecated as of saas-14.3,it is not needed for newer + versions of the mail plugin but necessary for supporting older versions """ return request.render('mail_client_extension.app_auth', values) - @http.route('/mail_client_extension/auth/confirm', type='http', auth="user", methods=['POST']) + @http.route(['/mail_client_extension/auth/confirm', '/mail_plugin/auth/confirm'], type='http', auth="user", methods=['POST']) def auth_confirm(self, scope, friendlyname, redirect, info=None, do=None, **kw): """ Called by the `app_auth` template. If the user decided to allow the app to access Odoo, a temporary auth code is generated and he is redirected to `redirect` with this code in the URL. It should redirect to the app, and the app should then exchange this auth code for an access token by calling - `/mail_client_extension/auth/access_token`. + `/mail_client/auth/access_token`. + + old route name "/mail_client_extension/auth/confirm is deprecated as of saas-14.3,it is not needed for newer + versions of the mail plugin but necessary for supporting older versions """ parsed_redirect = werkzeug.urls.url_parse(redirect) params = parsed_redirect.decode_query() @@ -39,19 +45,23 @@ class Authenticate(http.Controller): name = friendlyname if not info else f'{friendlyname}: {info}' auth_code = self._generate_auth_code(scope, name) # params is a MultiDict which does not support .update() with kwargs - params.update({'success': 1, 'auth_code': auth_code}) + # the state attribute is needed for the gmail connector + params.update({'success': 1, 'auth_code': auth_code, 'state': kw.get('state', '')}) else: - params['success'] = 0 + params.update({'success': 0, 'state': kw.get('state', '')}) updated_redirect = parsed_redirect.replace(query=werkzeug.urls.url_encode(params)) return werkzeug.utils.redirect(updated_redirect.to_url()) # In this case, an exception will be thrown in case of preflight request if only POST is allowed. - @http.route('/mail_client_extension/auth/access_token', type='json', auth="none", cors="*", + @http.route(['/mail_client_extension/auth/access_token', '/mail_plugin/auth/access_token'], type='json', auth="none", cors="*", methods=['POST', 'OPTIONS']) def auth_access_token(self, auth_code, **kw): """ Called by the external app to exchange an auth code, which is temporary and was passed in a URL, for an access token, which is permanent, and can be used in the `Authorization` header to authorize subsequent requests + + old route name "/mail_client_extension/auth/access_token is deprecated as of saas-14.3,it is not needed for newer + versions of the mail plugin but necessary for supporting older versions """ auth_message = self._get_auth_code_data(auth_code) if not auth_message: diff --git a/addons/mail_client_extension/controllers/mail_plugin.py b/addons/mail_client_extension/controllers/mail_plugin.py index ff7184d0ef0..d31f5394c4e 100644 --- a/addons/mail_client_extension/controllers/mail_plugin.py +++ b/addons/mail_client_extension/controllers/mail_plugin.py @@ -5,8 +5,9 @@ import base64 import json import logging import requests +from werkzeug.exceptions import Forbidden -from odoo import http, tools +from odoo import http, tools, _ from odoo.addons.iap.tools import iap_tools from odoo.http import request @@ -15,14 +16,165 @@ _logger = logging.getLogger(__name__) class MailPluginController(http.Controller): + @http.route('/mail_client_extension/modules/get', type="json", auth="outlook", csrf=False, cors="*") + def modules_get(self, **kwargs): + """ + deprecated as of saas-14.3, not needed for newer versions of the mail plugin but necessary + for supporting older versions + """ + return {'modules': ['contacts', 'crm']} + + @http.route('/mail_plugin/partner/enrich_and_create_company', + type="json", auth="outlook", cors="*") + def res_partner_enrich_and_create_company(self, partner_id): + """ + Route used when the user clicks on the create and enrich partner button + it will try to find a company using IAP, if a company is found + the enriched company will then be created in the database + """ + response = {} + + partner = request.env['res.partner'].browse(partner_id) + + if partner.parent_id: + return {'error': _("The partner already has a company related to him")} + + normalized_email = partner.email_normalized + if not normalized_email: + response = {'error': _('Contact has no valid email')} + return response + + company_domain = tools.email_domain_extract(normalized_email) + + company, enrichment_info = self._create_company_from_iap(company_domain) + + response['enrichment_info'] = enrichment_info + response['company'] = self._prepare_company_values(company) + if company: + partner.write({'parent_id': company}) + + return response + + @http.route(['/mail_client_extension/partner/get', '/mail_plugin/partner/get'] + , type="json", auth="outlook", cors="*") + def res_partner_get(self, email=None, name=None, partner_id=None, **kwargs): + """ + returns a partner given it's id or an email and a name. + In case the partner does not exist, we return partner having an id -1, we also look if an existing company + matching the contact exists in the database, if none is found a new company is enriched and created automatically + + old route name "/mail_client_extension/partner/get is deprecated as of saas-14.3, it is not needed for newer + versions of the mail plugin but necessary for supporting older versions, only the route name is deprecated not + the entire method. + """ + + if not (partner_id or (name and email)): + return {'error': _('You need to specify at least the partner_id or the name and the email')} + + if partner_id: + partner = request.env['res.partner'].browse(partner_id) + return self._prepare_contact_values(partner) + + normalized_email = tools.email_normalize(email) + if not normalized_email: + return {'error': _('Bad Email.')} + + # Search for the partner based on the email. + # If multiple are found, take the first one. + partner = request.env['res.partner'].search(['|', ('email', 'in', [normalized_email, email]), + ('email_normalized', '=', normalized_email)], limit=1) + + response = self._prepare_contact_values(partner) + + # if no partner is found in the database, we should also return an empty one having id = -1, otherwise older versions of + # plugin won't work + if not response['partner']: + sender_domain = tools.email_domain_extract(email) + response['partner'] = { + 'id': -1, + 'email': email, + 'name': name, + 'enrichment_info': None, + } + company = self._find_existing_company(normalized_email) + if not company: # create and enrich company + company, enrichment_info = self._create_company_from_iap(sender_domain) + response['partner']['enrichment_info'] = enrichment_info + response['partner']['company'] = self._prepare_company_values(company) + + return response + + @http.route('/mail_plugin/partner/search', type="json", auth="outlook", cors="*") + def res_partners_search(self, search_term, limit=30, **kwargs): + """ + Used for the plugin search contact functionality where the user types a string query in order to search for + matching contacts, the string query can either be the name of the contact, it's reference or it's email. + We choose these fields because these are probably the most interesting fields that the user can perform a + search on. + The method returns an array containing the dicts of the matched contacts. + """ + + #In a multi-company environment, the method may return contacts not belonging to the company that the user + #is connected to, this may result in the user not being able to view the contact in Odoo, while this may happen + #it is not supported for now and users are encouraged to check if they are connected to the correct company before + # clicking on a contact. + + normalized_email = tools.email_normalize(search_term) + + if normalized_email: + filter_domain = [('email_normalized', '=', search_term)] + else: + filter_domain = ['|', '|', ('display_name', 'ilike', search_term), ('ref', '=', search_term), + ('email', 'ilike', search_term)] + + # Search for the partner based on the email. + # If multiple are found, take the first one. + partners = request.env['res.partner'].search(filter_domain, limit=limit) + + partners = [ + self._prepare_partner_values(partner) + for partner in partners + ] + return {"partners": partners} + + @http.route(['/mail_client_extension/partner/create', '/mail_plugin/partner/create'], + type="json", auth="outlook", cors="*") + def res_partner_create(self, email, name, company): + """ + params email: email of the new partner + params name: name of the new partner + params company: parent company id of the new partner + """ + # old route name "/mail_client_extension/partner/create is deprecated as of saas-14.3,it is not needed for newer + # versions of the mail plugin but necessary for supporting older versions + # TODO search the company again instead of relying on the one provided here? + # Create the partner if needed. + partner_info = { + 'name': name, + 'email': email, + } + + #see if the partner has a parent company + if company and company > -1: + partner_info['parent_id'] = company + partner = request.env['res.partner'].create(partner_info) + + response = {'id': partner.id} + return response + + @http.route('/mail_plugin/log_mail_content', type="json", auth="outlook", cors="*") + def log_mail_content(self, model, res_id, message): + if model not in self._mail_content_logging_models_whitelist(): + raise Forbidden() + request.env[model].browse(res_id).message_post(body=message) + def _iap_enrich(self, domain): enriched_data = {} try: - response = request.env['iap.enrich.api']._request_enrich({domain: domain}) # The key doesn't matter - #except odoo.addons.iap.models.iap.InsufficientCreditError as ice: + response = request.env['iap.enrich.api']._request_enrich({domain: domain}) # The key doesn't matter except iap_tools.InsufficientCreditError: enriched_data['enrichment_info'] = {'type': 'insufficient_credit', 'info': request.env['iap.account'].get_credits_url('reveal')} - except Exception as e: + except Exception: enriched_data["enrichment_info"] = {'type': 'other', 'info': 'Unknown reason'} else: enriched_data = response.get(domain) @@ -30,35 +182,29 @@ class MailPluginController(http.Controller): enriched_data = {'enrichment_info': {'type': 'no_data', 'info': 'The enrichment API found no data for the email provided.'}} return enriched_data - @http.route('/mail_client_extension/modules/get', type="json", auth="outlook", csrf=False, cors="*") - def modules_get(self, **kwargs): - return {'modules': ['contacts', 'crm']} + def _find_existing_company(self, normalized_email): + sender_domain = tools.email_domain_extract(normalized_email) + search = sender_domain if sender_domain not in iap_tools._MAIL_DOMAIN_BLACKLIST else normalized_email + return request.env['res.partner'].search([('is_company', '=', True), ('email_normalized', '=ilike', '%' + search)], + limit=1) - # Find an existing company based on the email. - def _find_existing_company(self, domain): - if domain in iap_tools._MAIL_DOMAIN_BLACKLIST: - return - return request.env['res.partner'].search([('is_company', '=', True), ('email', '=ilike', '%' + domain)], limit=1) + def _prepare_company_values(self, company): - def _get_company_dict(self, company): if not company: return {'id': -1} - return { - 'id': company.id, - 'name': company.name, - 'phone': company.phone, - 'mobile': company.mobile, - 'email': company.email, - 'address': { - 'street': company.street, - 'city': company.city, - 'zip': company.zip, - 'country': company.country_id.name if company.country_id else '' - }, - 'website': company.website, - 'additionalInfo': json.loads(company.iap_enrich_info) if company.iap_enrich_info else {} - } + fields_list = ['id', 'name', 'phone', 'mobile', 'email', 'website'] + + company_values = dict((fname, company[fname]) for fname in fields_list) + company_values['address'] = {'street': company.street, + 'city': company.city, + 'zip': company.zip, + 'country': company.country_id.name if company.country_id else ''} + company_values['additionalInfo'] = json.loads(company.iap_enrich_info) if company.iap_enrich_info else {} + company_values['image'] = company.image_1920 + + return company_values + def _create_company_from_iap(self, domain): iap_data = self._iap_enrich(domain) @@ -69,7 +215,7 @@ class MailPluginController(http.Controller): emails = iap_data.get('email') new_company_info = { 'is_company': True, - 'name': iap_data.get("name"), + 'name': iap_data.get("name") or domain, 'street': iap_data.get("street_name"), 'city': iap_data.get("city"), 'zip': iap_data.get("postal_code"), @@ -85,7 +231,7 @@ class MailPluginController(http.Controller): if response.ok: new_company_info['image_1920'] = base64.b64encode(response.content) except Exception as e: - _logger.warning('Download of image for new company %r failed, error %r' % (new_company_info.name, e)) + _logger.warning('Download of image for new company %s failed, error %s', new_company_info.name, e) if iap_data.get('country_code'): country = request.env['res.country'].search([('code', '=', iap_data['country_code'].upper())]) @@ -93,8 +239,8 @@ class MailPluginController(http.Controller): new_company_info['country_id'] = country.id if iap_data.get('state_code'): state = request.env['res.country.state'].search([ - ('code', '=', iap_data['state_code']), - ('country_id', '=', country.id) + ('code', '=', iap_data['state_code']), + ('country_id', '=', country.id) ]) if state: new_company_info['state_id'] = state.id @@ -106,70 +252,42 @@ class MailPluginController(http.Controller): values=iap_data, subtype_id=request.env.ref('mail.mt_note').id, ) - + return new_company, {'type': 'company_created'} - @http.route('/mail_client_extension/partner/get', type="json", auth="outlook", cors="*") - def res_partner_get_by_email(self, email, name, **kwargs): - response = {} + def _prepare_partner_values(self, partner): - #compute the sender's domain - normalized_email = tools.email_normalize(email) - if not normalized_email: - response['error'] = 'Bad email.' - return response - sender_domain = normalized_email.split('@')[1] + fields_list = ['id', 'name', 'email', 'phone', 'mobile'] - # Search for the partner based on the email. - # If multiple are found, take the first one. - partner = request.env['res.partner'].search([('email', 'in', [normalized_email, email])], limit=1) + partner_values = dict((fname, partner[fname]) for fname in fields_list) + partner_values['image'] = partner.image_128 + partner_values['title'] = partner.function + partner_values['enrichment_info'] = None + + return partner_values + + + def _prepare_contact_values(self, partner): + """ + method used to return partner related values, it can be overridden by other modules if extra information have to + be returned with the partner (e.g., leads, ...) + """ if partner: - response['partner'] = { - 'id': partner.id, - 'name': partner.name, - 'title': partner.function, - 'email': partner.email, - 'image': partner.image_128, - 'phone': partner.phone, - 'mobile': partner.mobile, - 'enrichment_info': None - } - # if there is already a company for this partner, just take it without enrichment. - if partner.parent_id: - response['partner']['company'] = self._get_company_dict(partner.parent_id) + partner_response = self._prepare_partner_values(partner) + if partner.company_type == 'company': + partner_response['company'] = self._prepare_company_values(partner) + elif partner.parent_id: + partner_response['company'] = self._prepare_company_values(partner.parent_id) else: - company = self._find_existing_company(sender_domain) - if not company: # create and enrich company - company, enrichment_info = self._create_company_from_iap(sender_domain) - response['enrichment_info'] = enrichment_info - partner.write({'parent_id': company}) - response['partner']['company'] = self._get_company_dict(company) - else: #no partner found - response['partner'] = { - 'id': -1, - 'name': name, - 'email': email, - 'enrichment_info': None - } - company = self._find_existing_company(sender_domain) - if not company: # create and enrich company - company, enrichment_info = self._create_company_from_iap(sender_domain) - response['enrichment_info'] = enrichment_info - response['partner']['company'] = self._get_company_dict(company) + partner_response['company'] = self._prepare_company_values(None) + else: # no partner found + partner_response = {} - return response + return {'partner': partner_response} - @http.route('/mail_client_extension/partner/create', type="json", auth="outlook", cors="*") - def res_partner_create(self, email, name, company, **kwargs): - # TODO search the company again instead of relying on the one provided here? - # Create the partner if needed. - partner_info = { - 'name': name, - 'email': email, - } - if company > -1: - partner_info['parent_id'] = company - partner = request.env['res.partner'].create(partner_info) - - response = {'id': partner.id} - return response + def _mail_content_logging_models_whitelist(self): + """ + Returns all models that emails can be logged to and that can be used by the "log_mail_content" method, + it can be overridden by sub modules in order to whitelist more models + """ + return ['res.partner'] diff --git a/addons/mail_client_extension/views/mail_client_extension_login.xml b/addons/mail_client_extension/views/mail_client_extension_login.xml index 8d6918b20fe..6bc3d935954 100644 --- a/addons/mail_client_extension/views/mail_client_extension_login.xml +++ b/addons/mail_client_extension/views/mail_client_extension_login.xml @@ -7,6 +7,7 @@ +

Let access your Odoo database? @@ -18,4 +19,4 @@ - \ No newline at end of file + From 3d514037bb24f693102e12f5a3e2985917efa7d7 Mon Sep 17 00:00:00 2001 From: nounoubensebia Date: Fri, 26 Mar 2021 17:25:32 +0000 Subject: [PATCH 4/4] [REF] [crm_]mail_client_extension: rename to mail_plugin Rename the mail_client_extension and crm_mail_client_extension modules to mail_plugin and crm_mail_plugin respectively, purpose is to shorten the name of the modules as it is now quite big and verbose. Task-2427737 UPG-PR: https://github.com/odoo/upgrade/pull/2156 COM-PR: https://github.com/odoo/odoo/pull/66139 --- addons/base_setup/models/res_config_settings.py | 2 +- addons/base_setup/views/res_config_settings_views.xml | 6 +++--- .../__init__.py | 0 .../__manifest__.py | 6 +++--- .../controllers/__init__.py | 0 .../controllers/crm_client.py | 2 +- .../controllers/mail_plugin.py | 2 +- .../models/__init__.py | 0 .../models/crm_lead.py | 0 .../views/crm_lead_views.xml | 0 .../views/crm_mail_plugin_lead.xml} | 0 addons/{mail_client_extension => mail_plugin}/__init__.py | 0 .../{mail_client_extension => mail_plugin}/__manifest__.py | 4 ++-- .../controllers/__init__.py | 0 .../controllers/authenticate.py | 6 +++--- .../controllers/mail_plugin.py | 0 .../i18n/mail_client_extension.pot | 0 .../models/__init__.py | 0 .../models/ir_http.py | 2 +- .../models/res_partner.py | 0 .../views/mail_plugin_login.xml} | 4 ++-- 21 files changed, 17 insertions(+), 17 deletions(-) rename addons/{crm_mail_client_extension => crm_mail_plugin}/__init__.py (100%) rename addons/{crm_mail_client_extension => crm_mail_plugin}/__manifest__.py (82%) rename addons/{crm_mail_client_extension => crm_mail_plugin}/controllers/__init__.py (100%) rename addons/{crm_mail_client_extension => crm_mail_plugin}/controllers/crm_client.py (96%) rename addons/{crm_mail_client_extension => crm_mail_plugin}/controllers/mail_plugin.py (97%) rename addons/{crm_mail_client_extension => crm_mail_plugin}/models/__init__.py (100%) rename addons/{crm_mail_client_extension => crm_mail_plugin}/models/crm_lead.py (100%) rename addons/{crm_mail_client_extension => crm_mail_plugin}/views/crm_lead_views.xml (100%) rename addons/{crm_mail_client_extension/views/crm_mail_client_extension_lead.xml => crm_mail_plugin/views/crm_mail_plugin_lead.xml} (100%) rename addons/{mail_client_extension => mail_plugin}/__init__.py (100%) rename addons/{mail_client_extension => mail_plugin}/__manifest__.py (86%) rename addons/{mail_client_extension => mail_plugin}/controllers/__init__.py (100%) rename addons/{mail_client_extension => mail_plugin}/controllers/authenticate.py (96%) rename addons/{mail_client_extension => mail_plugin}/controllers/mail_plugin.py (100%) rename addons/{mail_client_extension => mail_plugin}/i18n/mail_client_extension.pot (100%) rename addons/{mail_client_extension => mail_plugin}/models/__init__.py (100%) rename addons/{mail_client_extension => mail_plugin}/models/ir_http.py (96%) rename addons/{mail_client_extension => mail_plugin}/models/res_partner.py (100%) rename addons/{mail_client_extension/views/mail_client_extension_login.xml => mail_plugin/views/mail_plugin_login.xml} (90%) diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py index ff6dc35bc3c..083d846d245 100644 --- a/addons/base_setup/models/res_config_settings.py +++ b/addons/base_setup/models/res_config_settings.py @@ -22,7 +22,7 @@ class ResConfigSettings(models.TransientModel): string='Allow the users to synchronize their calendar with Google Calendar') module_microsoft_calendar = fields.Boolean( string='Allow the users to synchronize their calendar with Outlook Calendar') - module_mail_client_extension = fields.Boolean( + module_mail_plugin = fields.Boolean( string='Allow integration with the mail plugins' ) module_google_drive = fields.Boolean("Attach Google documents to any record") diff --git a/addons/base_setup/views/res_config_settings_views.xml b/addons/base_setup/views/res_config_settings_views.xml index ecc361dc8fd..b27a1d51a52 100644 --- a/addons/base_setup/views/res_config_settings_views.xml +++ b/addons/base_setup/views/res_config_settings_views.xml @@ -210,12 +210,12 @@

Integrations

-
+
- +
-