From 1018c725715b2593a32614e50590eae9024f0220 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Tue, 25 Oct 2022 10:01:09 +0000 Subject: [PATCH 1/5] [IMP] mail, test_mail: allow model to provide partner default values Allow model to provide default values for auto creation of related res_partner such as name, title, company_id, ... It is used when claling 'Partner._find_or_create_from_emails()' that now accepts custom data when creating partners based on a given email_normalized. This allows notably to flatten a loop due to multi-company when creating partners from emails in template management. It is now done using this email based dict allowing to create all partners at once. Task-3024050 Part-of: odoo/odoo#105111 --- addons/mail/models/mail_template.py | 69 ++++++++++-------- addons/mail/models/mail_thread.py | 15 +++- addons/mail/models/res_partner.py | 17 ++++- addons/mail/tests/test_res_partner.py | 7 +- addons/test_mail/models/test_mail_models.py | 36 +++++++++ addons/test_mail/tests/test_mail_composer.py | 1 - .../test_mail/tests/test_mail_multicompany.py | 73 ++++++++++++++++++- .../tests/test_mail_thread_internals.py | 44 ++++++++--- 8 files changed, 213 insertions(+), 49 deletions(-) diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 9101525f99f..b14205a460b 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 +import itertools import logging from odoo import _, api, fields, models, tools, Command @@ -283,6 +284,13 @@ class MailTemplate(models.Model): (finding or creating partners). 'partner_to' field is transformed into 'partner_ids' field. + Note: for performance reason, information from records are transferred to + created partners no matter the company. For example, if we have a record of + company A and one of B with the same email and no related partner, a partner + will be created with company A or B but populated with information from the 2 + records. So some info might be leaked from one company to the other through + the partner. + :param list res_ids: list of record IDs on which template is rendered; :param list render_fields: list of fields to render on template which are specific to recipients, e.g. email_cc, email_to, partner_to); @@ -300,13 +308,12 @@ class MailTemplate(models.Model): self.ensure_one() if render_results is None: render_results = {} - records_sudo = None + ModelSudo = self.env[self.model].with_prefetch(res_ids).sudo() # if using default recipients -> ``_message_get_default_recipients`` gives # values for email_to, email_cc and partner_ids if self.use_default_to and self.model: - records_sudo = self.env[self.model].browse(res_ids).sudo() - default_recipients = records_sudo._message_get_default_recipients() + default_recipients = ModelSudo.browse(res_ids)._message_get_default_recipients() for res_id, recipients in default_recipients.items(): render_results.setdefault(res_id, {}).update(recipients) # render fields dynamically which generates recipients @@ -316,39 +323,39 @@ class MailTemplate(models.Model): for res_id in res_ids: render_results.setdefault(res_id, {})[field] = generated_field_values[res_id] - # classify records per company for partner fetch/creation - if find_or_create_partners and self.model and 'company_id' in self.env[self.model]._fields: - if not records_sudo: - records_sudo = self.env[self.model].browse(res_ids).sudo() - records_per_company = {} - for read_record in records_sudo.read(['company_id']): - company_id = read_record['company_id'][0] if read_record['company_id'] else False - records_per_company.setdefault(company_id, []).append(read_record['id']) - else: - records_per_company = {False: res_ids} - # create partners from emails if asked to if find_or_create_partners: - for company_id, record_ids in records_per_company.items(): - all_emails = [] - email_to_res_ids = {} - for res_id in record_ids: - record_values = render_results.setdefault(res_id, {}) - mails = tools.email_split(record_values.pop('email_to', '')) + \ - tools.email_split(record_values.pop('email_cc', '')) - all_emails += mails - for mail in mails: - email_to_res_ids.setdefault(mail, []).append(res_id) + res_id_to_company = {} + if self.model and 'company_id' in ModelSudo._fields: + for read_record in ModelSudo.browse(res_ids).read(['company_id']): + company_id = read_record['company_id'][0] if read_record['company_id'] else False + res_id_to_company[read_record['id']] = company_id - if not all_emails: - continue - additional_values = {} - if company_id: - additional_values['company_id'] = company_id + all_emails = [] + email_to_res_ids = {} + email_to_company = {} + for res_id in res_ids: + record_values = render_results.setdefault(res_id, {}) + mails = tools.email_split(record_values.pop('email_to', '')) + \ + tools.email_split(record_values.pop('email_cc', '')) + all_emails += mails + record_company = res_id_to_company.get(res_id) + for mail in mails: + email_to_res_ids.setdefault(mail, []).append(res_id) + if record_company: + email_to_company[mail] = record_company + + if all_emails: + customers_information = ModelSudo.browse(res_ids)._get_customer_information() partners = self.env['res.partner']._find_or_create_from_emails( all_emails, - additional_values=additional_values - ) + additional_values={ + email: { + 'company_id': email_to_company.get(email), + **customers_information.get(email, {}), + } + for email in itertools.chain(all_emails, [False]) + }) for original_email, partner in zip(all_emails, partners): if not partner: continue diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index b8260ddddfa..a3a82797d38 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1609,7 +1609,7 @@ class MailThread(models.AbstractModel): def _message_add_suggested_recipient(self, result, partner=None, email=None, lang=None, reason=''): """ Called by _message_get_suggested_recipients, to add a suggested recipient in the result dictionary. The form is : - partner_id, partner_name or partner_name, reason """ + partner_id, partner_name or partner_name, lang, reason """ self.ensure_one() if email and not partner: # get partner info from email @@ -1793,6 +1793,19 @@ class MailThread(models.AbstractModel): ]).write({'author_id': partner.id}) return result + + def _get_customer_information(self): + """ Get customer information that can be extracted from the records by + normalized email. + + The goal of this method is to offer an extension point to subclasses + for retrieving initial values from a record to populate related + customers record (res_partner). + + :return dict: normalized email -> dict of initial res_partner values + """ + return {} + # ------------------------------------------------------------ # MESSAGE POST MAIN # ------------------------------------------------------------ diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index cf821a0dbcb..165d99c4fe5 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -108,6 +108,11 @@ class Partner(models.Model): can be given to newly created partners. If an email is not unique (e.g. multi-email input), only the first found email is considered. + Additional values allow to customize the created partner when context + allows to give more information. It data is based on email normalized + as it is the main information used in this method to distinguish or + find partners. + If no valid email is found for a given item, the given value is used to find partners with same invalid email or create a new one with the wrong value. It allows updating it afterwards. Notably with notifications @@ -116,12 +121,16 @@ class Partner(models.Model): :param list emails: list of emails that may be formatted (each input will be parsed and normalized); - :param dict additional_values: additional values given to create if - the partner is not found. Typically used to propagate a company_id; + :param dict additional_values: additional values per normalized email + given to create if the partner is not found. Typically used to + propagate a company_id and customer information from related record. + Values for key 'False' are used when creating partner for invalid + emails; :return: res.partner records in a list, following order of emails. It is not a recordset, to keep Falsy values. """ + additional_values = additional_values if additional_values else {} partners, tocreate_vals_list = self.env['res.partner'], [] name_emails = [self._parse_partner_name(email) for email in emails] @@ -161,7 +170,7 @@ class Partner(models.Model): { self._rec_name: name or email_normalized, 'email': email_normalized, - **(additional_values or {}) + **additional_values.get(email_normalized, {}), } for name, email_normalized in notfound_name_emails ] @@ -172,7 +181,7 @@ class Partner(models.Model): { self._rec_name: name, 'email': name, - **(additional_values or {}) + **additional_values.get(False, {}), } for name in names if name not in partners.mapped('email') ] diff --git a/addons/mail/tests/test_res_partner.py b/addons/mail/tests/test_res_partner.py index 061840cacb7..1ebfdb2f235 100644 --- a/addons/mail/tests/test_res_partner.py +++ b/addons/mail/tests/test_res_partner.py @@ -186,7 +186,12 @@ class TestPartner(MailCommon): samples_emails = [item[0] for item in new_samples] partners = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails( samples_emails, - additional_values={'company_id': self.env.company.id}, + additional_values={ + tools.email_normalize(email): { + 'company_id': self.env.company.id, + } + for email in samples_emails + }, ) self.assertEqual(len(partners), len(new_samples)) for (sample, exp_name, exp_email), partner in zip(new_samples, partners): diff --git a/addons/test_mail/models/test_mail_models.py b/addons/test_mail/models/test_mail_models.py index eb3c90bea26..62f2494e1c4 100644 --- a/addons/test_mail/models/test_mail_models.py +++ b/addons/test_mail/models/test_mail_models.py @@ -2,6 +2,8 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models +from odoo.tools import email_normalize +from odoo.tools.translate import _ class MailTestSimple(models.Model): @@ -127,6 +129,8 @@ class MailTestTicket(models.Model): name = fields.Char() email_from = fields.Char(tracking=True) + mobile_number = fields.Char() + phone_number = fields.Char() count = fields.Integer(default=1) datetime = fields.Datetime(default=fields.Datetime.now) mail_template = fields.Many2one('mail.template', 'Template') @@ -193,7 +197,39 @@ class MailTestTicket(models.Model): return self.env.ref('test_mail.st_mail_test_ticket_container_upd') return super(MailTestTicket, self)._track_subtype(init_values) + def _get_customer_information(self): + email_normalized_to_values = super()._get_customer_information() + for record in self.filtered('email_from'): + email_from_normalized = email_normalize(record.email_from) + if not email_from_normalized: # do not fill Falsy with random data + continue + values = email_normalized_to_values.setdefault(email_from_normalized, {}) + if not values.get('mobile'): + values['mobile'] = record.mobile_number + if not values.get('phone'): + values['phone'] = record.phone_number + return email_normalized_to_values + + def _message_get_suggested_recipients(self): + recipients = super()._message_get_suggested_recipients() + for ticket in self: + if ticket.customer_id: + ticket.customer_id._message_add_suggested_recipient( + recipients, + partner=ticket.customer_id, + lang=None, + reason=_('Customer'), + ) + elif ticket.email_from: + ticket._message_add_suggested_recipient( + recipients, + partner=None, + email=self.email_from, + lang=None, + reason=_('Customer Email'), + ) + return recipients class MailTestTicketEL(models.Model): """ Just mail.test.ticket, but exclusion-list enabled. Kept as different diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index 77d892f61a9..d71d9f769e0 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -1494,7 +1494,6 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): self.assertTrue(all(attach not in message.attachment_ids for attach in attachs), 'Should have copied attachments') - @tagged('mail_composer', 'mail_blacklist') class TestComposerResultsCommentStatus(TestMailComposer): """ Test cases involving blacklist, opt-out, state management, ... specific diff --git a/addons/test_mail/tests/test_mail_multicompany.py b/addons/test_mail/tests/test_mail_multicompany.py index 58309a7482f..657e2ecd740 100644 --- a/addons/test_mail/tests/test_mail_multicompany.py +++ b/addons/test_mail/tests/test_mail_multicompany.py @@ -3,10 +3,12 @@ import socket +from itertools import product + from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients from odoo.tests import tagged from odoo.tests.common import users -from odoo.tools import formataddr +from odoo.tools import formataddr, mute_logger @tagged('multi_company') @@ -47,6 +49,75 @@ class TestMultiCompanySetup(TestMailCommon, TestRecipients): 'message_id': '<123456-openerp-%s-mail.test.gateway@%s>' % (cls.test_record.id, socket.gethostname()), }) + @users('erp_manager') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + def test_mail_composer_wtpl_populate_new_recipient(self): + """ Test auto-populate of auto created partner with related record + values when sending a mail with a template. + """ + self.user_erp_manager.write({ + 'groups_id': [(4, self.env.ref('base.group_partner_manager').id)], + }) + companies = self.company_admin + self.company_2 + test_records = self.env['mail.test.ticket.mc'].create([ + { + 'email_from': f'newpartner{idx}@example.com', + 'company_id': companies[idx].id, + 'customer_id': False, + 'mobile_number': f'+3319900{idx:02d}{idx:02d}', + 'name': f'TestRecord{idx}', + 'phone_number': False, + 'user_id': False, + } for idx in range(2) + ]) + manual_recipients = test_records.mapped('email_from') + template = self.env['mail.template'].create({ + 'email_to': '{{ object.email_from }}', + 'model_id': self.env['ir.model']._get_id(test_records._name), + 'partner_to': False, + }) + + for composition_mode, batch_mode in product( + ('comment', 'mass_mail'), + (True, False) + ): + with self.subTest(composition_mode=composition_mode, batch_mode=batch_mode): + test_records = test_records if batch_mode else test_records[0] + + self.assertFalse( + self.env['res.partner'].search( + [('email_normalized', 'in', manual_recipients)] + ) + ) + ctx = { + 'default_composition_mode': composition_mode, + 'default_model': test_records._name, + 'default_res_ids': test_records.ids, + } + composer = self.env['mail.compose.message'].with_context(ctx).create({ + 'template_id': template.id, + }) + + with self.mock_mail_gateway(): + composer._action_send_mail() + + new_partners = self.env['res.partner'].search([('email_normalized', 'in', manual_recipients)], + order='email') + try: + self.assertEqual( + len(new_partners), len(test_records) + ) + self.assertEqual( + new_partners.mapped('company_id'), + test_records.mapped('company_id') + ) + self.assertEqual( + new_partners.mapped('mobile'), + test_records.mapped('mobile_number') + ) + finally: + new_partners.unlink() + @users('employee') def test_notify_reply_to_computation(self): test_record = self.env['mail.test.gateway'].browse(self.test_record.ids) diff --git a/addons/test_mail/tests/test_mail_thread_internals.py b/addons/test_mail/tests/test_mail_thread_internals.py index 36d254e3868..0a618ae78a9 100644 --- a/addons/test_mail/tests/test_mail_thread_internals.py +++ b/addons/test_mail/tests/test_mail_thread_internals.py @@ -296,16 +296,6 @@ class TestDiscuss(TestMailCommon, TestRecipients): self.assertFalse(msg.starred) self.assertTrue(msg_emp.starred) - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_mail_cc_recipient_suggestion(self): - record = self.env['mail.test.cc'].create({'email_cc': 'cc1@example.com, cc2@example.com, cc3 '}) - suggestions = record._message_get_suggested_recipients()[record.id] - self.assertEqual(sorted(suggestions), [ - (False, '"cc3" ', None, 'CC Email'), - (False, 'cc1@example.com', None, 'CC Email'), - (False, 'cc2@example.com', None, 'CC Email'), - ], 'cc should be in suggestions') - def test_inbox_message_fetch_needaction(self): user1 = self.env['res.users'].create({'login': 'user1', 'name': 'User 1'}) user1.notification_type = 'inbox' @@ -356,6 +346,40 @@ class TestDiscuss(TestMailCommon, TestRecipients): threads_admin = self.test_record.with_user(self.user_admin).search([('message_has_error', '=', True)]) self.assertEqual(len(threads_admin), 0) + @users("employee") + def test_suggested_recipients_default_create_value(self): + """ Test default creation values returned for suggested recipient. """ + email = 'newpartner@example.com' + data_from_record_mobile = '+33199001015' + record = self.env['mail.test.ticket'].create({ + 'email_from': email, + 'mobile_number': data_from_record_mobile, + }) + suggestions = record._message_get_suggested_recipients()[record.id] + self.assertEqual( + suggestions, + [(False, email, None, 'Customer Email')] + ) + + @users("employee") + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_suggested_recipients_mail_cc(self): + """ MailThreadCC mixin adds its own suggested recipients management + coming from CC (carbon copy) management. """ + record = self.env['mail.test.cc'].create({ + 'email_cc': 'cc1@example.com, cc2@example.com, cc3 ', + }) + suggestions = record._message_get_suggested_recipients()[record.id] + self.assertEqual( + sorted(suggestions), + [ + (False, '"cc3" ', None, 'CC Email'), + (False, 'cc1@example.com', None, 'CC Email'), + (False, 'cc2@example.com', None, 'CC Email'), + ], + 'cc should be in suggestions' + ) + @users("employee") def test_unlink_notification_message(self): channel = self.env['mail.channel'].create({'name': 'testChannel'}) From a1d508f7baa799fb5fadccfc1feb4f3922b9bd09 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Tue, 25 Oct 2022 10:02:00 +0000 Subject: [PATCH 2/5] [IMP] crm: provide crm_lead values for related res_partner creation Define information taken from the lead when a partner is automatically created from it. It avoids the user to enter the information twice and automated partner creation now correctly takes information when possible. Task-3024050 Part-of: odoo/odoo#105111 --- addons/crm/models/crm_lead.py | 15 ++++++++ addons/crm/tests/test_crm_lead.py | 61 +++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 85468e3973d..22fb969c361 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -1822,6 +1822,21 @@ class Lead(models.Model): return partner_company return Partner.create(self._prepare_customer_values(self.name, is_company=False)) + def _get_customer_information(self): + email_normalized_to_values = super()._get_customer_information() + Partner = self.env['res.partner'] + + for record in self.filtered('email_normalized'): + values = email_normalized_to_values.setdefault(record.email_normalized, {}) + contact_name = record.contact_name or record.partner_name or ( + Partner._parse_partner_name(record.email_from)[0] or record.email_from) + # Note that we don't attempt to create the parent company even if partner name is set + values.update(record._prepare_customer_values(contact_name, is_company=False)) + values['company_name'] = record.partner_name + if contact_name == record.partner_name: + values['company_type'] = 'company' + return email_normalized_to_values + def _prepare_customer_values(self, partner_name, is_company=False, parent_id=False): """ Extract data from lead to create a partner. diff --git a/addons/crm/tests/test_crm_lead.py b/addons/crm/tests/test_crm_lead.py index 049f77a9621..1b4d6c7c446 100644 --- a/addons/crm/tests/test_crm_lead.py +++ b/addons/crm/tests/test_crm_lead.py @@ -10,7 +10,7 @@ from odoo.addons.crm.tests.common import TestCrmCommon, INCOMING_EMAIL from odoo.addons.phone_validation.tools.phone_validation import phone_format from odoo.exceptions import UserError from odoo.tests.common import Form, tagged, users -from odoo.tools import mute_logger +from odoo.tools import formataddr, mute_logger @tagged('lead_internals') @@ -663,7 +663,12 @@ class TestCRMLead(TestCrmCommon): 'lang_id': lang_fr.id, }) data = lead1._message_get_suggested_recipients()[lead1.id] - self.assertEqual(data, [(False, self.test_email, None, 'Customer Email')]) + self.assertEqual(len(data), 1) + suggested_partner_id, suggested_partner_email, suggested_lang, suggested_reason = data[0] + self.assertFalse(suggested_partner_id) + self.assertEqual(suggested_partner_email, self.test_email) + self.assertEqual(suggested_lang, None) + self.assertEqual(suggested_reason, 'Customer Email') # Create a lead with an active language -> should keep the preset language for recipients lang_en = ResLang.search([('code', '=', 'en_US')]) @@ -678,7 +683,57 @@ class TestCRMLead(TestCrmCommon): 'lang_id': lang_en.id, }) data = lead2._message_get_suggested_recipients()[lead2.id] - self.assertEqual(data, [(False, self.test_email, "en_US", 'Customer Email')]) + self.assertEqual(len(data), 1) + suggested_partner_id, suggested_partner_email, suggested_lang, suggested_reason = data[0] + self.assertFalse(suggested_partner_id) + self.assertEqual(suggested_partner_email, self.test_email) + self.assertEqual(suggested_lang, 'en_US') + self.assertEqual(suggested_reason, 'Customer Email') + + @users('user_sales_manager') + def test_message_get_suggested_recipients_auto_creation(self): + """Check default creates value for auto creation of recipient + (customer).""" + lead_details_for_contact = { + 'title': self.env.ref('base.res_partner_title_mister').id, + 'street': '3rd Floor, Room 3-C', + 'street2': '123 Arlington Avenue', + 'zip': '13202', + 'city': 'New York', + 'country_id': self.env.ref('base.us').id, + 'state_id': self.env.ref('base.state_us_39').id, + 'website': 'https://www.arlington123.com/3f3c', + 'phone': '678-728-0949', + 'mobile': '661-606-0781', + 'function': 'Delivery Boy', + 'team_id': self.sales_team_1.id, + 'user_id': self.user_sales_manager.id, + } + + for partner_name, name, email in [ + (False, 'Test', 'test_default_create@example.com'), + ('Delivery Boy company', 'Test With Company', 'default_create_with_partner@example.com'), + ('Delivery Boy company', '', 'default_create_with_partner_no_name@example.com'), + ]: + formatted_email = formataddr((name, email)) + with self.subTest(partner_name=partner_name): + lang = self.env['res.lang'].sudo().search([], limit=1)[0] + description = '

Top

' + lead1 = self.env['crm.lead'].create({ + 'name': 'TestLead', + 'contact_name': name, + 'email_from': formatted_email, + 'lang_id': lang.id, + 'description': description, + 'partner_name': partner_name, + **lead_details_for_contact, + }) + data = lead1._message_get_suggested_recipients()[lead1.id] + suggested_partner_id, suggested_partner_email, suggested_lang, suggested_reason = data[0] + self.assertFalse(suggested_partner_id) + self.assertEqual(suggested_partner_email, formatted_email) + self.assertEqual(suggested_lang, lang.code) + self.assertEqual(suggested_reason, 'Customer Email') @users('user_sales_manager') def test_phone_mobile_search(self): From 31811ba89f6d2a2875ae19a1131a4c2182b71c89 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Thu, 27 Oct 2022 14:50:38 +0000 Subject: [PATCH 3/5] [IMP] base, crm, mail: prefill partner creation form with current record data Selecting a recipient for which there exists no partner trigger a partner form dialog creation. For some model (ex.: crm.lead), some default values can be prefilled from the current record. This is what we do here by calling a generic mechanism that allows each model to define default value for related partner. Note that we don't provide a more detailed form to the user because some will otherwise feel obligated to fill it out entirely. Technical note: As there is no need for a custom form per model from which the data are extracted to populate the related partner, the simplified partner form (view_partner_simple_form) is just augmented with additional invisible field by modules that add fields to partner and need to populate them automatically from values of another model. Technical note: In TestCRMLead.test_message_recipient_partner_auto_creation, we test that the _message_get_suggested_recipients returns the correct default values to copy some fields from the lead to the partner to avoid the user to reenter them. But we don't test specifically that it also work when using mail composer and template (which generate the partner) because it has already been tested when introducing the mechanism. Although, we verify that it should behave the same because we verify that the default values returned by _get_customer_information, that is used in mail template when using the composer, are the same as the ones returned by _message_get_suggested_recipients which is tested here. Task-3024050 Part-of: odoo/odoo#105111 --- addons/crm/tests/test_crm_lead.py | 23 ++++++++++++++++--- addons/crm/views/res_partner_views.xml | 13 +++++++++++ addons/mail/models/mail_thread.py | 13 +++++++---- .../composer_suggested_recipient.js | 3 +++ .../src/models/suggested_recipient_info.js | 7 ++++++ addons/mail/static/src/models/thread.js | 3 ++- .../tests/test_mail_thread_internals.py | 8 +++---- odoo/addons/base/views/res_partner_views.xml | 16 +++++++++++-- 8 files changed, 71 insertions(+), 15 deletions(-) diff --git a/addons/crm/tests/test_crm_lead.py b/addons/crm/tests/test_crm_lead.py index 1b4d6c7c446..c3e245c22eb 100644 --- a/addons/crm/tests/test_crm_lead.py +++ b/addons/crm/tests/test_crm_lead.py @@ -664,11 +664,12 @@ class TestCRMLead(TestCrmCommon): }) data = lead1._message_get_suggested_recipients()[lead1.id] self.assertEqual(len(data), 1) - suggested_partner_id, suggested_partner_email, suggested_lang, suggested_reason = data[0] + suggested_partner_id, suggested_partner_email, suggested_lang, suggested_reason, create_vals = data[0] self.assertFalse(suggested_partner_id) self.assertEqual(suggested_partner_email, self.test_email) self.assertEqual(suggested_lang, None) self.assertEqual(suggested_reason, 'Customer Email') + self.assertEqual(create_vals['lang'], 'fr_FR') # Create a lead with an active language -> should keep the preset language for recipients lang_en = ResLang.search([('code', '=', 'en_US')]) @@ -684,11 +685,12 @@ class TestCRMLead(TestCrmCommon): }) data = lead2._message_get_suggested_recipients()[lead2.id] self.assertEqual(len(data), 1) - suggested_partner_id, suggested_partner_email, suggested_lang, suggested_reason = data[0] + suggested_partner_id, suggested_partner_email, suggested_lang, suggested_reason, create_vals = data[0] self.assertFalse(suggested_partner_id) self.assertEqual(suggested_partner_email, self.test_email) self.assertEqual(suggested_lang, 'en_US') self.assertEqual(suggested_reason, 'Customer Email') + self.assertEqual(create_vals['lang'], lang_en.code) @users('user_sales_manager') def test_message_get_suggested_recipients_auto_creation(self): @@ -729,11 +731,26 @@ class TestCRMLead(TestCrmCommon): **lead_details_for_contact, }) data = lead1._message_get_suggested_recipients()[lead1.id] - suggested_partner_id, suggested_partner_email, suggested_lang, suggested_reason = data[0] + suggested_partner_id, suggested_partner_email, suggested_lang, suggested_reason, create_vals = data[0] self.assertFalse(suggested_partner_id) self.assertEqual(suggested_partner_email, formatted_email) self.assertEqual(suggested_lang, lang.code) self.assertEqual(suggested_reason, 'Customer Email') + self.assertEqual(create_vals, lead1._get_customer_information().get(email, {})) + for field, value in lead_details_for_contact.items(): + self.assertEqual(create_vals.get(field), value) + expected_name = partner_name if partner_name and not name else name + self.assertEqual(create_vals['name'], expected_name) + self.assertEqual(create_vals['comment'], description) # description -> comment + # Parent company not created even if partner_name is set + self.assertFalse(create_vals.get('parent_id')) # not supported, even if partner_name set + self.assertEqual(create_vals['company_name'], partner_name) # partner_name -> company_name + expected_company_type = 'company' if partner_name and not name else 'person' + self.assertEqual(create_vals.get('company_type', 'person'), expected_company_type) + + # Check that the creation of the contact won't fail + partner = self.env['res.partner'].create(create_vals) + partner.unlink() @users('user_sales_manager') def test_phone_mobile_search(self): diff --git a/addons/crm/views/res_partner_views.xml b/addons/crm/views/res_partner_views.xml index c3b02a18ef0..5f09112bc10 100644 --- a/addons/crm/views/res_partner_views.xml +++ b/addons/crm/views/res_partner_views.xml @@ -47,4 +47,17 @@ + + res.partner.view.form.simple.form.crm + res.partner + + + + + + + + + + diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index a3a82797d38..18960992d0e 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1609,7 +1609,8 @@ class MailThread(models.AbstractModel): def _message_add_suggested_recipient(self, result, partner=None, email=None, lang=None, reason=''): """ Called by _message_get_suggested_recipients, to add a suggested recipient in the result dictionary. The form is : - partner_id, partner_name or partner_name, lang, reason """ + partner_id, partner_name or partner_name, lang, + reason, create_values """ self.ensure_one() if email and not partner: # get partner info from email @@ -1623,16 +1624,18 @@ class MailThread(models.AbstractModel): if partner and partner.id in [val[0] for val in result[self.ids[0]]]: # already existing partner ID -> skip return result if partner and partner.email: # complete profile: id, name - result[self.ids[0]].append((partner.id, partner.email_formatted, lang, reason)) + result[self.ids[0]].append((partner.id, partner.email_formatted, lang, reason, {})) elif partner: # incomplete profile: id, name - result[self.ids[0]].append((partner.id, '%s' % (partner.name), lang, reason)) + result[self.ids[0]].append((partner.id, '%s' % (partner.name), lang, reason, {})) else: # unknown partner, we are probably managing an email address - result[self.ids[0]].append((False, email, lang, reason)) + _, parsed_email = self.env['res.partner']._parse_partner_name(email) + partner_create_values = self._get_customer_information().get(parsed_email, {}) + result[self.ids[0]].append((False, email, lang, reason, partner_create_values)) return result def _message_get_suggested_recipients(self): """ Returns suggested recipients for ids. Those are a list of - tuple (partner_id, partner_name, reason), to be managed by Chatter. """ + tuple (partner_id, partner_name, reason, default_create_value), to be managed by Chatter. """ result = dict((res_id, []) for res_id in self.ids) if 'user_id' in self._fields: for obj in self.sudo(): # SUPERUSER because of a read on res.users that would crash otherwise diff --git a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js index 8bf6cba6e33..3f2533a2b2e 100644 --- a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js +++ b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js @@ -78,6 +78,9 @@ export class ComposerSuggestedRecipientView extends Component { .name, default_lang: this.composerSuggestedRecipientView.suggestedRecipientInfo .lang, + ...(Object.fromEntries(Object.entries( + this.composerSuggestedRecipientView.suggestedRecipientInfo.defaultCreateValues) + .map(([k, v]) => ['default_' + k, v]))), force_email: true, ref: "compound_context", }, diff --git a/addons/mail/static/src/models/suggested_recipient_info.js b/addons/mail/static/src/models/suggested_recipient_info.js index 81ca18af343..b14d4162c2d 100644 --- a/addons/mail/static/src/models/suggested_recipient_info.js +++ b/addons/mail/static/src/models/suggested_recipient_info.js @@ -10,6 +10,13 @@ Model({ composerSuggestedRecipientViews: many("ComposerSuggestedRecipientView", { inverse: "suggestedRecipientInfo", }), + /** + * Default values for partner creation (empty object if partner is set). + */ + defaultCreateValues: attr({ + default: {}, + readonly: true, + }), dialogText: attr({ compute() { return this.env._t("Please complete customer's information"); diff --git a/addons/mail/static/src/models/thread.js b/addons/mail/static/src/models/thread.js index 657f643749a..f326bae7bb6 100644 --- a/addons/mail/static/src/models/thread.js +++ b/addons/mail/static/src/models/thread.js @@ -558,7 +558,7 @@ Model({ } if (suggestedRecipientsData) { const recipientInfoList = suggestedRecipientsData.map((recipientInfoData) => { - const [partner_id, emailInfo, lang, reason] = recipientInfoData; + const [partner_id, emailInfo, lang, reason, defaultCreateValues] = recipientInfoData; const [name, email] = emailInfo && mailUtils.parseEmail(emailInfo); return { email, @@ -567,6 +567,7 @@ Model({ lang, partner: partner_id ? insert({ id: partner_id }) : clear(), reason, + defaultCreateValues: defaultCreateValues, }; }); Object.assign(values, { diff --git a/addons/test_mail/tests/test_mail_thread_internals.py b/addons/test_mail/tests/test_mail_thread_internals.py index 0a618ae78a9..8148b4a1ad5 100644 --- a/addons/test_mail/tests/test_mail_thread_internals.py +++ b/addons/test_mail/tests/test_mail_thread_internals.py @@ -358,7 +358,7 @@ class TestDiscuss(TestMailCommon, TestRecipients): suggestions = record._message_get_suggested_recipients()[record.id] self.assertEqual( suggestions, - [(False, email, None, 'Customer Email')] + [(False, email, None, 'Customer Email', {'mobile': '+33199001015', 'phone': False})] ) @users("employee") @@ -373,9 +373,9 @@ class TestDiscuss(TestMailCommon, TestRecipients): self.assertEqual( sorted(suggestions), [ - (False, '"cc3" ', None, 'CC Email'), - (False, 'cc1@example.com', None, 'CC Email'), - (False, 'cc2@example.com', None, 'CC Email'), + (False, '"cc3" ', None, 'CC Email', {}), + (False, 'cc1@example.com', None, 'CC Email', {}), + (False, 'cc2@example.com', None, 'CC Email', {}), ], 'cc should be in suggestions' ) diff --git a/odoo/addons/base/views/res_partner_views.xml b/odoo/addons/base/views/res_partner_views.xml index c69b9dbbb8c..49e5be43943 100644 --- a/odoo/addons/base/views/res_partner_views.xml +++ b/odoo/addons/base/views/res_partner_views.xml @@ -81,10 +81,22 @@ res.partner
- - + + + + + + + + + + + + + +
From 69011e329ccded51052fd02b3b40b067369f3e42 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Wed, 21 Dec 2022 13:21:37 +0000 Subject: [PATCH 4/5] [IMP] mail: avoid displaying twice the email in the composer recipients When the contact name was not yet set, the email was displayed twice in the composer: first as the contact name and then as the email in parentheses. Now when the contact is the same as the email, nothing is displayed in parentheses. Technical note: the regexp in the mail util parseEmail method has been slightly modified because it was returning in some cases the name surrounded by quotes (ex.: "newlead3@ex.com" --> name: "newlead3@ex.com"). This is no longer the case with this change. That change was needed for comparing the name and the email to avoid to display the same thing twice (when the email and the name are the same). Note that the method is only used once, where we have done the change. Task-3024050 Part-of: odoo/odoo#105111 --- .../composer_suggested_recipient.xml | 2 +- addons/mail/static/src/js/utils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.xml b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.xml index c27b6d74cdc..3c02468459a 100644 --- a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.xml +++ b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.xml @@ -6,7 +6,7 @@ diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js index 3049ecac935..8e94c665351 100644 --- a/addons/mail/static/src/js/utils.js +++ b/addons/mail/static/src/js/utils.js @@ -173,7 +173,7 @@ function inline(node, transform_children) { // Parses text to find email: Tagada -> [Tagada, address@mail.fr] or False function parseEmail(text) { if (text) { - var result = text.match(/(.*)<(.*@.*)>/); + var result = text.match(/"?(.*?)"? <(.*@.*)>/); if (result) { return [_.str.trim(result[1]), _.str.trim(result[2])]; } From 3f413ca3dad0778f490d340fc74d18f81405090d Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Wed, 11 Jan 2023 08:02:48 +0000 Subject: [PATCH 5/5] [FIX] mail: fix incorrect view form for partner auto creation This fixes the problem of the full res_partner form being displayed instead of the simplified one in some cases (and vice-versa). The problem can be reproduced by the following steps: - launch odoo without xml dev_mode enabled (otherwise the cache is disabled) - in helpdesk, go to an existing ticket and click on an existing customer (this will load the full form for the customer into cache) - create a new ticket by first completing name and customer name, save it - complete the email address, save it - go to "Send message" and click on the email checkbox - the full form is loaded instead of the simplified one (base.view_partner_simple_form) It is also possible to get the simple form instead of the full form by reversing the order of the visited forms. Technical note: The problem has been fixed by adding the context key "force_email" as cache key using _get_view_cache_key override. Indeed, _get_view_cache uses _get_view which result depends on that context variable in its redefinition in res_partner. Task-3024050 Part-of: odoo/odoo#105111 --- addons/mail/models/res_partner.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 165d99c4fe5..6edb1893dac 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -75,6 +75,11 @@ class Partner(models.Model): # ------------------------------------------------------------ # ORM # ------------------------------------------------------------ + @api.model + def _get_view_cache_key(self, view_id=None, view_type='form', **options): + """Add context variable force_email in the key as _get_view depends on it.""" + key = super()._get_view_cache_key(view_id, view_type, **options) + return key + (self._context.get('force_email'),) @api.model @api.returns('self', lambda value: value.id)