summaryrefslogtreecommitdiff
path: root/addons/crm/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/crm/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/crm/tests')
-rw-r--r--addons/crm/tests/__init__.py11
-rw-r--r--addons/crm/tests/common.py385
-rw-r--r--addons/crm/tests/test_crm_activity.py177
-rw-r--r--addons/crm/tests/test_crm_lead.py487
-rw-r--r--addons/crm/tests/test_crm_lead_convert.py484
-rw-r--r--addons/crm/tests/test_crm_lead_convert_mass.py212
-rw-r--r--addons/crm/tests/test_crm_lead_lost.py65
-rw-r--r--addons/crm/tests/test_crm_lead_merge.py143
-rw-r--r--addons/crm/tests/test_crm_lead_notification.py135
-rw-r--r--addons/crm/tests/test_crm_pls.py401
-rw-r--r--addons/crm/tests/test_crm_ui.py24
11 files changed, 2524 insertions, 0 deletions
diff --git a/addons/crm/tests/__init__.py b/addons/crm/tests/__init__.py
new file mode 100644
index 00000000..793d217c
--- /dev/null
+++ b/addons/crm/tests/__init__.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+
+from . import test_crm_lead
+from . import test_crm_lead_notification
+from . import test_crm_lead_convert
+from . import test_crm_lead_convert_mass
+from . import test_crm_lead_lost
+from . import test_crm_lead_merge
+from . import test_crm_activity
+from . import test_crm_ui
+from . import test_crm_pls
diff --git a/addons/crm/tests/common.py b/addons/crm/tests/common.py
new file mode 100644
index 00000000..18e56378
--- /dev/null
+++ b/addons/crm/tests/common.py
@@ -0,0 +1,385 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from unittest.mock import patch
+
+from odoo.addons.mail.tests.common import MailCase, mail_new_test_user
+from odoo.addons.sales_team.tests.common import TestSalesCommon
+from odoo.fields import Datetime
+from odoo import tools
+
+INCOMING_EMAIL = """Return-Path: {return_path}
+X-Original-To: {to}
+Delivered-To: {to}
+Received: by mail.my.com (Postfix, from userid xxx)
+ id 822ECBFB67; Mon, 24 Oct 2011 07:36:51 +0200 (CEST)
+X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on mail.my.com
+X-Spam-Level:
+X-Spam-Status: No, score=-1.0 required=5.0 tests=ALL_TRUSTED autolearn=ham
+ version=3.3.1
+Received: from [192.168.1.146]
+ (Authenticated sender: {email_from})
+ by mail.customer.com (Postfix) with ESMTPSA id 07A30BFAB4
+ for <{to}>; Mon, 24 Oct 2011 07:36:50 +0200 (CEST)
+Message-ID: {msg_id}
+Date: Mon, 24 Oct 2011 11:06:29 +0530
+From: {email_from}
+User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.14) Gecko/20110223 Lightning/1.0b2 Thunderbird/3.1.8
+MIME-Version: 1.0
+To: {to}
+Subject: {subject}
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 8bit
+
+This is an example email. All sensitive content has been stripped out.
+
+ALL GLORY TO THE HYPNOTOAD !
+
+Cheers,
+
+Somebody."""
+
+
+class TestCrmCommon(TestSalesCommon, MailCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestCrmCommon, cls).setUpClass()
+ cls._init_mail_gateway()
+
+ cls.sales_team_1.write({
+ 'alias_name': 'sales.test',
+ 'use_leads': True,
+ 'use_opportunities': True,
+ })
+
+ (cls.user_sales_manager | cls.user_sales_leads | cls.user_sales_salesman).write({
+ 'groups_id': [(4, cls.env.ref('crm.group_use_lead').id)]
+ })
+
+ cls.env['crm.stage'].search([]).write({'sequence': 9999}) # ensure search will find test data first
+ cls.stage_team1_1 = cls.env['crm.stage'].create({
+ 'name': 'New',
+ 'sequence': 1,
+ 'team_id': cls.sales_team_1.id,
+ })
+ cls.stage_team1_2 = cls.env['crm.stage'].create({
+ 'name': 'Proposition',
+ 'sequence': 5,
+ 'team_id': cls.sales_team_1.id,
+ })
+ cls.stage_team1_won = cls.env['crm.stage'].create({
+ 'name': 'Won',
+ 'sequence': 70,
+ 'team_id': cls.sales_team_1.id,
+ 'is_won': True,
+ })
+ cls.stage_gen_1 = cls.env['crm.stage'].create({
+ 'name': 'Generic stage',
+ 'sequence': 3,
+ 'team_id': False,
+ })
+ cls.stage_gen_won = cls.env['crm.stage'].create({
+ 'name': 'Generic Won',
+ 'sequence': 30,
+ 'team_id': False,
+ 'is_won': True,
+ })
+
+ cls.lead_1 = cls.env['crm.lead'].create({
+ 'name': 'Nibbler Spacecraft Request',
+ 'type': 'lead',
+ 'user_id': cls.user_sales_leads.id,
+ 'team_id': cls.sales_team_1.id,
+ 'partner_id': False,
+ 'contact_name': 'Amy Wong',
+ 'email_from': 'amy.wong@test.example.com',
+ 'country_id': cls.env.ref('base.us').id,
+ })
+ # update lead_1: stage_id is not computed anymore by default for leads
+ cls.lead_1.write({
+ 'stage_id': cls.stage_team1_1.id,
+ })
+
+ # create an history for new team
+ cls.lead_team_1_won = cls.env['crm.lead'].create({
+ 'name': 'Already Won',
+ 'type': 'lead',
+ 'user_id': cls.user_sales_leads.id,
+ 'team_id': cls.sales_team_1.id,
+ })
+ cls.lead_team_1_won.action_set_won()
+ cls.lead_team_1_lost = cls.env['crm.lead'].create({
+ 'name': 'Already Won',
+ 'type': 'lead',
+ 'user_id': cls.user_sales_leads.id,
+ 'team_id': cls.sales_team_1.id,
+ })
+ cls.lead_team_1_lost.action_set_lost()
+ (cls.lead_team_1_won | cls.lead_team_1_lost).flush()
+
+ # email / phone data
+ cls.test_email_data = [
+ '"Planet Express" <planet.express@test.example.com>',
+ '"Philip, J. Fry" <philip.j.fry@test.example.com>',
+ '"Turanga Leela" <turanga.leela@test.example.com>',
+ ]
+ cls.test_email_data_normalized = [
+ 'planet.express@test.example.com',
+ 'philip.j.fry@test.example.com',
+ 'turanga.leela@test.example.com',
+ ]
+ cls.test_phone_data = [
+ '+1 202 555 0122', # formatted US number
+ '202 555 0999', # local US number
+ '202 555 0888', # local US number
+ ]
+ cls.test_phone_data_sanitized = [
+ '+12025550122',
+ '+12025550999',
+ '+12025550888',
+ ]
+
+ # create some test contact and companies
+ cls.contact_company_1 = cls.env['res.partner'].create({
+ 'name': 'Planet Express',
+ 'email': cls.test_email_data[0],
+ 'is_company': True,
+ 'street': '57th Street',
+ 'city': 'New New York',
+ 'country_id': cls.env.ref('base.us').id,
+ 'zip': '12345',
+ })
+ cls.contact_1 = cls.env['res.partner'].create({
+ 'name': 'Philip J Fry',
+ 'email': cls.test_email_data[1],
+ 'mobile': cls.test_phone_data[0],
+ 'title': cls.env.ref('base.res_partner_title_mister').id,
+ 'function': 'Delivery Boy',
+ 'phone': False,
+ 'parent_id': cls.contact_company_1.id,
+ 'is_company': False,
+ 'street': 'Actually the sewers',
+ 'city': 'New York',
+ 'country_id': cls.env.ref('base.us').id,
+ 'zip': '54321',
+ })
+ cls.contact_2 = cls.env['res.partner'].create({
+ 'name': 'Turanga Leela',
+ 'email': cls.test_email_data[2],
+ 'mobile': cls.test_phone_data[1],
+ 'phone': cls.test_phone_data[2],
+ 'parent_id': False,
+ 'is_company': False,
+ 'street': 'Cookieville Minimum-Security Orphanarium',
+ 'city': 'New New York',
+ 'country_id': cls.env.ref('base.us').id,
+ 'zip': '97648',
+ })
+
+ def _create_leads_batch(self, lead_type='lead', count=10, partner_ids=None, user_ids=None):
+ """ Helper tool method creating a batch of leads, useful when dealing
+ with batch processes. Please update me.
+
+ :param string type: 'lead', 'opportunity', 'mixed' (lead then opp),
+ None (depends on configuration);
+ """
+ types = ['lead', 'opportunity']
+ leads_data = [{
+ 'name': 'TestLead_%02d' % (x),
+ 'type': lead_type if lead_type else types[x % 2],
+ 'priority': '%s' % (x % 3),
+ } for x in range(count)]
+
+ # customer information
+ if partner_ids:
+ for idx, lead_data in enumerate(leads_data):
+ lead_data['partner_id'] = partner_ids[idx % len(partner_ids)]
+ else:
+ for idx, lead_data in enumerate(leads_data):
+ lead_data['email_from'] = tools.formataddr((
+ 'TestCustomer_%02d' % (idx),
+ 'customer_email_%02d@example.com' % (idx)
+ ))
+
+ # salesteam information
+ if user_ids:
+ for idx, lead_data in enumerate(leads_data):
+ lead_data['user_id'] = user_ids[idx % len(user_ids)]
+
+ return self.env['crm.lead'].create(leads_data)
+
+ def _create_duplicates(self, lead, create_opp=True):
+ """ Helper tool method creating, based on a given lead
+
+ * a customer (res.partner) based on lead email (to test partner finding)
+ -> FIXME: using same normalized email does not work currently, only exact email works
+ * a lead with same email_from
+ * a lead with same email_normalized (other email_from)
+ * a lead with customer but another email
+ * a lost opportunity with same email_from
+ """
+ self.customer = self.env['res.partner'].create({
+ 'name': 'Lead1 Email Customer',
+ 'email': lead.email_from,
+ })
+ self.lead_email_from = self.env['crm.lead'].create({
+ 'name': 'Duplicate: same email_from',
+ 'type': 'lead',
+ 'team_id': lead.team_id.id,
+ 'email_from': lead.email_from,
+ })
+ # self.lead_email_normalized = self.env['crm.lead'].create({
+ # 'name': 'Duplicate: email_normalize comparison',
+ # 'type': 'lead',
+ # 'team_id': lead.team_id.id,
+ # 'stage_id': lead.stage_id.id,
+ # 'email_from': 'CUSTOMER WITH NAME <%s>' % lead.email_normalized.upper(),
+ # })
+ self.lead_partner = self.env['crm.lead'].create({
+ 'name': 'Duplicate: customer ID',
+ 'type': 'lead',
+ 'team_id': lead.team_id.id,
+ 'partner_id': self.customer.id,
+ })
+ if create_opp:
+ self.opp_lost = self.env['crm.lead'].create({
+ 'name': 'Duplicate: lost opportunity',
+ 'type': 'opportunity',
+ 'team_id': lead.team_id.id,
+ 'stage_id': lead.stage_id.id,
+ 'email_from': lead.email_from,
+ })
+ self.opp_lost.action_set_lost()
+ else:
+ self.opp_lost = self.env['crm.lead']
+
+ # self.assertEqual(self.lead_email_from.email_normalized, self.lead_email_normalized.email_normalized)
+ # self.assertTrue(lead.email_from != self.lead_email_normalized.email_from)
+ # self.assertFalse(self.opp_lost.active)
+
+ # new_lead = self.lead_email_from | self.lead_email_normalized | self.lead_partner | self.opp_lost
+ new_leads = self.lead_email_from | self.lead_partner | self.opp_lost
+ new_leads.flush() # compute notably probability
+ return new_leads
+
+
+class TestLeadConvertCommon(TestCrmCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestLeadConvertCommon, cls).setUpClass()
+ # Sales Team organization
+ # Role: M (team member) R (team manager)
+ # SALESMAN---------------sales_team_1-----sales_team_convert
+ # admin------------------M----------------/
+ # user_sales_manager-----R----------------R
+ # user_sales_leads-------M----------------/
+ # user_sales_salesman----/----------------M
+
+ # Stages Team organization
+ # Name-------------------ST-------------------Sequ
+ # stage_team1_1----------sales_team_1---------1
+ # stage_team1_2----------sales_team_1---------5
+ # stage_team1_won--------sales_team_1---------70
+ # stage_gen_1------------/--------------------3
+ # stage_gen_won----------/--------------------30
+ # stage_team_convert_1---sales_team_convert---1
+
+ cls.sales_team_convert = cls.env['crm.team'].create({
+ 'name': 'Convert Sales Team',
+ 'sequence': 10,
+ 'alias_name': False,
+ 'use_leads': True,
+ 'use_opportunities': True,
+ 'company_id': False,
+ 'user_id': cls.user_sales_manager.id,
+ 'member_ids': [(4, cls.user_sales_salesman.id)],
+ })
+ cls.stage_team_convert_1 = cls.env['crm.stage'].create({
+ 'name': 'New',
+ 'sequence': 1,
+ 'team_id': cls.sales_team_convert.id,
+ })
+
+ cls.lead_1.write({'date_open': Datetime.from_string('2020-01-15 11:30:00')})
+
+ cls.crm_lead_dt_patcher = patch('odoo.addons.crm.models.crm_lead.fields.Datetime', wraps=Datetime)
+ cls.crm_lead_dt_mock = cls.crm_lead_dt_patcher.start()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.crm_lead_dt_patcher.stop()
+ super(TestLeadConvertCommon, cls).tearDownClass()
+
+
+class TestLeadConvertMassCommon(TestLeadConvertCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestLeadConvertMassCommon, cls).setUpClass()
+ # Sales Team organization
+ # Role: M (team member) R (team manager)
+ # SALESMAN-------------------sales_team_1-----sales_team_convert
+ # admin----------------------M----------------/
+ # user_sales_manager---------R----------------R
+ # user_sales_leads-----------M----------------/
+ # user_sales_leads_convert---/----------------M <-- NEW
+ # user_sales_salesman--------/----------------M
+
+ cls.user_sales_leads_convert = mail_new_test_user(
+ cls.env, login='user_sales_leads_convert',
+ name='Lucien Sales Leads Convert', email='crm_leads_2@test.example.com',
+ company_id=cls.env.ref("base.main_company").id,
+ notification_type='inbox',
+ groups='sales_team.group_sale_salesman_all_leads,base.group_partner_manager,crm.group_use_lead',
+ )
+ cls.sales_team_convert.write({
+ 'member_ids': [(4, cls.user_sales_leads_convert.id)]
+ })
+
+ cls.lead_w_partner = cls.env['crm.lead'].create({
+ 'name': 'New1',
+ 'type': 'lead',
+ 'probability': 10,
+ 'user_id': cls.user_sales_manager.id,
+ 'stage_id': False,
+ 'partner_id': cls.contact_1.id,
+ })
+ cls.lead_w_partner.write({'stage_id': False})
+ cls.lead_w_partner_company = cls.env['crm.lead'].create({
+ 'name': 'New1',
+ 'type': 'lead',
+ 'probability': 15,
+ 'user_id': cls.user_sales_manager.id,
+ 'stage_id': cls.stage_team1_1.id,
+ 'partner_id': cls.contact_company_1.id,
+ 'contact_name': 'Hermes Conrad',
+ 'email_from': 'hermes.conrad@test.example.com',
+ })
+ cls.lead_w_contact = cls.env['crm.lead'].create({
+ 'name': 'LeadContact',
+ 'type': 'lead',
+ 'probability': 15,
+ 'contact_name': 'TestContact',
+ 'user_id': cls.user_sales_salesman.id,
+ 'stage_id': cls.stage_gen_1.id,
+ })
+ cls.lead_w_email = cls.env['crm.lead'].create({
+ 'name': 'LeadEmailAsContact',
+ 'type': 'lead',
+ 'probability': 15,
+ 'email_from': 'contact.email@test.example.com',
+ 'user_id': cls.user_sales_salesman.id,
+ 'stage_id': cls.stage_gen_1.id,
+ })
+ cls.lead_w_email_lost = cls.env['crm.lead'].create({
+ 'name': 'Lost',
+ 'type': 'lead',
+ 'probability': 15,
+ 'email_from': 'strange.from@test.example.com',
+ 'user_id': cls.user_sales_leads.id,
+ 'stage_id': cls.stage_team1_2.id,
+ 'active': False,
+ })
+ (cls.lead_w_partner | cls.lead_w_partner_company | cls.lead_w_contact | cls.lead_w_email | cls.lead_w_email_lost).flush()
diff --git a/addons/crm/tests/test_crm_activity.py b/addons/crm/tests/test_crm_activity.py
new file mode 100644
index 00000000..44eb0310
--- /dev/null
+++ b/addons/crm/tests/test_crm_activity.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import date, timedelta
+
+from odoo.addons.crm.tests.common import TestCrmCommon
+from odoo.tests.common import users
+
+
+class TestCrmMailActivity(TestCrmCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestCrmMailActivity, cls).setUpClass()
+
+ cls.activity_type_1 = cls.env['mail.activity.type'].create({
+ 'name': 'Initial Contact',
+ 'delay_count': 5,
+ 'summary': 'ACT 1 : Presentation, barbecue, ... ',
+ 'res_model_id': cls.env['ir.model']._get('crm.lead').id,
+ })
+ cls.activity_type_2 = cls.env['mail.activity.type'].create({
+ 'name': 'Call for Demo',
+ 'delay_count': 6,
+ 'summary': 'ACT 2 : I want to show you my ERP !',
+ 'res_model_id': cls.env['ir.model']._get('crm.lead').id,
+ })
+ for activity_type in cls.activity_type_1 + cls.activity_type_2:
+ cls.env['ir.model.data'].create({
+ 'name': activity_type.name.lower().replace(' ', '_'),
+ 'module': 'crm',
+ 'model': activity_type._name,
+ 'res_id': activity_type.id,
+ })
+
+ @users('user_sales_leads')
+ def test_crm_activity_ordering(self):
+ """ Test ordering on "my activities", linked to a hack introduced for a b2b.
+ Purpose is to be able to order on "my activities", which is a filtered o2m.
+ In this test we will check search, limit, order and offset linked to the
+ override of search for this non stored computed field. """
+ # Initialize some batch data
+ default_order = self.env['crm.lead']._order
+ self.assertEqual(default_order, "priority desc, id desc") # force updating this test is order changes
+ test_leads = self._create_leads_batch(count=10, partner_ids=[self.contact_1.id, self.contact_2.id, False]).sorted('id')
+
+ # assert initial data, ensure we did not break base behavior
+ for lead in test_leads:
+ self.assertFalse(lead.activity_date_deadline_my)
+ search_res = self.env['crm.lead'].search([('id', 'in', test_leads.ids)], limit=5, offset=0, order='id ASC')
+ self.assertEqual(search_res.ids, test_leads[:5].ids)
+ search_res = self.env['crm.lead'].search([('id', 'in', test_leads.ids)], limit=5, offset=5, order='id ASC')
+ self.assertEqual(search_res.ids, test_leads[5:10].ids)
+
+ # Let's schedule some activities for "myself" and "my bro"and check those are correctly computed
+ # LEAD NUMBER DEADLINE (MY) PRIORITY LATE DEADLINE (MY)
+ # 0 +2D 0 +2D
+ # 1 -1D 1 +2D
+ # 2 -2D 2 +2D
+ # 3 -1D 0 -1D
+ # 4 -2D 1 -2D
+ # 5 +2D 2 +2D
+ # 6+ / 0/1/2/0 /
+ today = date.today()
+ deadline_in2d, deadline_in1d = today + timedelta(days=2), today + timedelta(days=1)
+ deadline_was2d, deadline_was1d = today + timedelta(days=-2), today + timedelta(days=-1)
+ deadlines_my = [deadline_in2d, deadline_was1d, deadline_was2d, deadline_was1d, deadline_was2d,
+ deadline_in2d, False, False, False, False]
+ deadlines_gl = [deadline_in1d, deadline_was1d, deadline_was2d, deadline_was1d, deadline_was2d,
+ deadline_in2d, False, False, False, False]
+
+ test_leads[0:4].activity_schedule(act_type_xmlid='crm.call_for_demo', user_id=self.user_sales_manager.id, date_deadline=deadline_in1d)
+ test_leads[0:3].activity_schedule(act_type_xmlid='crm.initial_contact', date_deadline=deadline_in2d)
+ test_leads[5].activity_schedule(act_type_xmlid='crm.initial_contact', date_deadline=deadline_in2d)
+ (test_leads[1] | test_leads[3]).activity_schedule(act_type_xmlid='crm.initial_contact', date_deadline=deadline_was1d)
+ (test_leads[2] | test_leads[4]).activity_schedule(act_type_xmlid='crm.call_for_demo', date_deadline=deadline_was2d)
+ test_leads.invalidate_cache()
+
+ expected_ids_asc = [2, 4, 1, 3, 5, 0, 8, 7, 9, 6]
+ expected_leads_asc = self.env['crm.lead'].browse([test_leads[lid].id for lid in expected_ids_asc])
+ expected_ids_desc = [5, 0, 1, 3, 2, 4, 8, 7, 9, 6]
+ expected_leads_desc = self.env['crm.lead'].browse([test_leads[lid].id for lid in expected_ids_desc])
+
+ for idx, lead in enumerate(test_leads):
+ self.assertEqual(lead.activity_date_deadline_my, deadlines_my[idx])
+ self.assertEqual(lead.activity_date_deadline, deadlines_gl[idx], 'Fail at %s' % idx)
+
+ # Let's go for a first batch of search
+ _order = 'activity_date_deadline_my ASC, %s' % default_order
+ _domain = [('id', 'in', test_leads.ids)]
+
+ search_res = self.env['crm.lead'].search(_domain, limit=None, offset=0, order=_order)
+ self.assertEqual(expected_leads_asc.ids, search_res.ids)
+ search_res = self.env['crm.lead'].search(_domain, limit=4, offset=0, order=_order)
+ self.assertEqual(expected_leads_asc[:4].ids, search_res.ids)
+ search_res = self.env['crm.lead'].search(_domain, limit=4, offset=3, order=_order)
+ self.assertEqual(expected_leads_asc[3:7].ids, search_res.ids)
+ search_res = self.env['crm.lead'].search(_domain, limit=None, offset=3, order=_order)
+ self.assertEqual(expected_leads_asc[3:].ids, search_res.ids)
+
+ _order = 'activity_date_deadline_my DESC, %s' % default_order
+ search_res = self.env['crm.lead'].search(_domain, limit=None, offset=0, order=_order)
+ self.assertEqual(expected_leads_desc.ids, search_res.ids)
+ search_res = self.env['crm.lead'].search(_domain, limit=4, offset=0, order=_order)
+ self.assertEqual(expected_leads_desc[:4].ids, search_res.ids)
+ search_res = self.env['crm.lead'].search(_domain, limit=4, offset=3, order=_order)
+ self.assertEqual(expected_leads_desc[3:7].ids, search_res.ids)
+ search_res = self.env['crm.lead'].search(_domain, limit=None, offset=3, order=_order)
+ self.assertEqual(expected_leads_desc[3:].ids, search_res.ids)
+
+ def test_crm_activity_recipients(self):
+ """ This test case checks
+ - no internal subtype followed by client
+ - activity subtype are not default ones
+ - only activity followers are recipients when this kind of activity is logged
+ """
+ # Add explicitly a the client as follower
+ self.lead_1.message_subscribe([self.contact_1.id])
+
+ # Check the client is not follower of any internal subtype
+ internal_subtypes = self.lead_1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.contact_1).mapped('subtype_ids').filtered(lambda subtype: subtype.internal)
+ self.assertFalse(internal_subtypes)
+
+ # Add sale manager as follower of default subtypes
+ self.lead_1.message_subscribe([self.user_sales_manager.partner_id.id], subtype_ids=[self.env.ref('mail.mt_activities').id, self.env.ref('mail.mt_comment').id])
+
+ activity = self.env['mail.activity'].with_user(self.user_sales_leads).create({
+ 'activity_type_id': self.activity_type_1.id,
+ 'note': 'Content of the activity to log',
+ 'res_id': self.lead_1.id,
+ 'res_model_id': self.env.ref('crm.model_crm_lead').id,
+ })
+ activity._onchange_activity_type_id()
+ self.assertEqual(self.lead_1.activity_type_id, self.activity_type_1)
+ self.assertEqual(self.lead_1.activity_summary, self.activity_type_1.summary)
+ # self.assertEqual(self.lead.activity_date_deadline, self.activity_type_1.summary)
+
+ # mark as done, check lead and posted message
+ activity.action_done()
+ self.assertFalse(self.lead_1.activity_type_id.id)
+ self.assertFalse(self.lead_1.activity_ids)
+ activity_message = self.lead_1.message_ids[0]
+ self.assertEqual(activity_message.notified_partner_ids, self.user_sales_manager.partner_id)
+ self.assertEqual(activity_message.subtype_id, self.env.ref('mail.mt_activities'))
+
+ def test_crm_activity_next_action(self):
+ """ This test case set the next activity on a lead, log another, and schedule a third. """
+ # Add the next activity (like we set it from a form view)
+ lead_model_id = self.env['ir.model']._get('crm.lead').id
+ activity = self.env['mail.activity'].with_user(self.user_sales_manager).create({
+ 'activity_type_id': self.activity_type_1.id,
+ 'summary': 'My Own Summary',
+ 'res_id': self.lead_1.id,
+ 'res_model_id': lead_model_id,
+ })
+ activity._onchange_activity_type_id()
+
+ # Check the next activity is correct
+ self.assertEqual(self.lead_1.activity_summary, activity.summary)
+ self.assertEqual(self.lead_1.activity_type_id, activity.activity_type_id)
+ # self.assertEqual(fields.Datetime.from_string(self.lead.activity_date_deadline), datetime.now() + timedelta(days=activity.activity_type_id.days))
+
+ activity.write({
+ 'activity_type_id': self.activity_type_2.id,
+ 'summary': '',
+ 'note': 'Content of the activity to log',
+ })
+ activity._onchange_activity_type_id()
+
+ self.assertEqual(self.lead_1.activity_summary, activity.activity_type_id.summary)
+ self.assertEqual(self.lead_1.activity_type_id, activity.activity_type_id)
+ # self.assertEqual(fields.Datetime.from_string(self.lead.activity_date_deadline), datetime.now() + timedelta(days=activity.activity_type_id.days))
+
+ activity.action_done()
+
+ # Check the next activity on the lead has been removed
+ self.assertFalse(self.lead_1.activity_type_id)
diff --git a/addons/crm/tests/test_crm_lead.py b/addons/crm/tests/test_crm_lead.py
new file mode 100644
index 00000000..f0a26716
--- /dev/null
+++ b/addons/crm/tests/test_crm_lead.py
@@ -0,0 +1,487 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.crm.models.crm_lead import PARTNER_FIELDS_TO_SYNC, PARTNER_ADDRESS_FIELDS_TO_SYNC
+from odoo.addons.crm.tests.common import TestCrmCommon, INCOMING_EMAIL
+from odoo.addons.phone_validation.tools.phone_validation import phone_format
+from odoo.tests.common import Form, users
+
+
+class TestCRMLead(TestCrmCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestCRMLead, cls).setUpClass()
+ cls.country_ref = cls.env.ref('base.be')
+ cls.test_email = '"Test Email" <test.email@example.com>'
+ cls.test_phone = '0485112233'
+
+ def assertLeadAddress(self, lead, street, street2, city, lead_zip, state, country):
+ self.assertEqual(lead.street, street)
+ self.assertEqual(lead.street2, street2)
+ self.assertEqual(lead.city, city)
+ self.assertEqual(lead.zip, lead_zip)
+ self.assertEqual(lead.state_id, state)
+ self.assertEqual(lead.country_id, country)
+
+ @users('user_sales_leads')
+ def test_crm_lead_contact_fields_mixed(self):
+ """ Test mixed configuration from partner: both user input and coming
+ from partner, in order to ensure we do not loose information or make
+ it incoherent. """
+ lead_data = {
+ 'name': 'TestMixed',
+ 'partner_id': self.contact_1.id,
+ # address
+ 'country_id': self.country_ref.id,
+ # other contact fields
+ 'function': 'Parmesan Rappeur',
+ # specific contact fields
+ 'email_from': self.test_email,
+ 'phone': self.test_phone,
+ }
+ lead = self.env['crm.lead'].create(lead_data)
+ # classic
+ self.assertEqual(lead.name, "TestMixed")
+ # address
+ self.assertLeadAddress(lead, False, False, False, False, self.env['res.country.state'], self.country_ref)
+ # other contact fields
+ for fname in set(PARTNER_FIELDS_TO_SYNC) - set(['function']):
+ self.assertEqual(lead[fname], self.contact_1[fname], 'No user input -> take from contact for field %s' % fname)
+ self.assertEqual(lead.function, 'Parmesan Rappeur', 'User input should take over partner value')
+ # specific contact fields
+ self.assertEqual(lead.partner_name, self.contact_company_1.name)
+ self.assertEqual(lead.contact_name, self.contact_1.name)
+ self.assertEqual(lead.email_from, self.test_email)
+ self.assertEqual(lead.phone, self.test_phone)
+
+ # update a single address fields -> only those are updated
+ lead.write({'street': 'Super Street', 'city': 'Super City'})
+ self.assertLeadAddress(lead, 'Super Street', False, 'Super City', False, self.env['res.country.state'], self.country_ref)
+
+ # change partner -> whole address updated
+ lead.write({'partner_id': self.contact_company_1.id})
+ for fname in PARTNER_ADDRESS_FIELDS_TO_SYNC:
+ self.assertEqual(lead[fname], self.contact_company_1[fname])
+
+ @users('user_sales_leads')
+ def test_crm_lead_creation_no_partner(self):
+ lead_data = {
+ 'name': 'Test',
+ 'country_id': self.country_ref.id,
+ 'email_from': self.test_email,
+ 'phone': self.test_phone,
+ }
+ lead = self.env['crm.lead'].new(lead_data)
+ # get the street should not trigger cache miss
+ lead.street
+ # Create the lead and the write partner_id = False: country should remain
+ lead = self.env['crm.lead'].create(lead_data)
+ self.assertEqual(lead.country_id, self.country_ref, "Country should be set on the lead")
+ self.assertEqual(lead.email_from, self.test_email)
+ self.assertEqual(lead.phone, self.test_phone)
+ lead.partner_id = False
+ self.assertEqual(lead.country_id, self.country_ref, "Country should still be set on the lead")
+ self.assertEqual(lead.email_from, self.test_email)
+ self.assertEqual(lead.phone, self.test_phone)
+
+ @users('user_sales_manager')
+ def test_crm_lead_creation_partner(self):
+ lead = self.env['crm.lead'].create({
+ 'name': 'TestLead',
+ 'contact_name': 'Raoulette TestContact',
+ 'email_from': '"Raoulette TestContact" <raoulette@test.example.com>',
+ })
+ self.assertEqual(lead.type, 'lead')
+ self.assertEqual(lead.user_id, self.user_sales_manager)
+ self.assertEqual(lead.team_id, self.sales_team_1)
+ self.assertEqual(lead.stage_id, self.stage_team1_1)
+ self.assertEqual(lead.contact_name, 'Raoulette TestContact')
+ self.assertEqual(lead.email_from, '"Raoulette TestContact" <raoulette@test.example.com>')
+
+ # update to a partner, should udpate address
+ lead.write({'partner_id': self.contact_1.id})
+ self.assertEqual(lead.partner_name, self.contact_company_1.name)
+ self.assertEqual(lead.contact_name, self.contact_1.name)
+ self.assertEqual(lead.email_from, self.contact_1.email)
+ self.assertEqual(lead.street, self.contact_1.street)
+ self.assertEqual(lead.city, self.contact_1.city)
+ self.assertEqual(lead.zip, self.contact_1.zip)
+ self.assertEqual(lead.country_id, self.contact_1.country_id)
+
+ def test_crm_lead_creation_partner_address(self):
+ """ Test that an address erases all lead address fields (avoid mixed addresses) """
+ other_country = self.env.ref('base.fr')
+ empty_partner = self.env['res.partner'].create({
+ 'name': 'Empty partner',
+ 'country_id': other_country.id,
+ })
+ lead_data = {
+ 'name': 'Test',
+ 'street': 'My street',
+ 'street2': 'My street',
+ 'city': 'My city',
+ 'zip': 'test@odoo.com',
+ 'state_id': self.env['res.country.state'].create({
+ 'name': 'My state',
+ 'country_id': self.country_ref.id,
+ 'code': 'MST',
+ }).id,
+ 'country_id': self.country_ref.id,
+ }
+ lead = self.env['crm.lead'].create(lead_data)
+ lead.partner_id = empty_partner
+ # PARTNER_ADDRESS_FIELDS_TO_SYNC
+ self.assertEqual(lead.street, empty_partner.street, "Street should be sync from the Partner")
+ self.assertEqual(lead.street2, empty_partner.street2, "Street 2 should be sync from the Partner")
+ self.assertEqual(lead.city, empty_partner.city, "City should be sync from the Partner")
+ self.assertEqual(lead.zip, empty_partner.zip, "Zip should be sync from the Partner")
+ self.assertEqual(lead.state_id, empty_partner.state_id, "State should be sync from the Partner")
+ self.assertEqual(lead.country_id, empty_partner.country_id, "Country should be sync from the Partner")
+
+ def test_crm_lead_creation_partner_no_address(self):
+ """ Test that an empty address on partner does not void its lead values """
+ empty_partner = self.env['res.partner'].create({
+ 'name': 'Empty partner',
+ 'is_company': True,
+ 'mobile': '123456789',
+ 'title': self.env.ref('base.res_partner_title_mister').id,
+ 'function': 'My function',
+ })
+ lead_data = {
+ 'name': 'Test',
+ 'contact_name': 'Test',
+ 'street': 'My street',
+ 'country_id': self.country_ref.id,
+ 'email_from': self.test_email,
+ 'phone': self.test_phone,
+ 'mobile': '987654321',
+ 'website': 'http://mywebsite.org',
+ }
+ lead = self.env['crm.lead'].create(lead_data)
+ lead.partner_id = empty_partner
+ # SPECIFIC FIELDS
+ self.assertEqual(lead.contact_name, lead_data['contact_name'], "Contact should remain")
+ self.assertEqual(lead.email_from, lead_data['email_from'], "Email From should keep its initial value")
+ self.assertEqual(lead.partner_name, empty_partner.name, "Partner name should be set as contact is a company")
+ # PARTNER_ADDRESS_FIELDS_TO_SYNC
+ self.assertEqual(lead.street, lead_data['street'], "Street should remain since partner has no address field set")
+ self.assertEqual(lead.street2, False, "Street2 should remain since partner has no address field set")
+ self.assertEqual(lead.country_id, self.country_ref, "Country should remain since partner has no address field set")
+ self.assertEqual(lead.city, False, "City should remain since partner has no address field set")
+ self.assertEqual(lead.zip, False, "Zip should remain since partner has no address field set")
+ self.assertEqual(lead.state_id, self.env['res.country.state'], "State should remain since partner has no address field set")
+ # PARTNER_FIELDS_TO_SYNC
+ self.assertEqual(lead.phone, lead_data['phone'], "Phone should keep its initial value")
+ self.assertEqual(lead.mobile, empty_partner.mobile, "Mobile from partner should be set on the lead")
+ self.assertEqual(lead.title, empty_partner.title, "Title from partner should be set on the lead")
+ self.assertEqual(lead.function, empty_partner.function, "Function from partner should be set on the lead")
+ self.assertEqual(lead.website, lead_data['website'], "Website should keep its initial value")
+
+ @users('user_sales_manager')
+ def test_crm_lead_partner_sync(self):
+ lead, partner = self.lead_1.with_user(self.env.user), self.contact_2
+ partner_email, partner_phone = self.contact_2.email, self.contact_2.phone
+ lead.partner_id = partner
+
+ # email & phone must be automatically set on the lead
+ lead.partner_id = partner
+ self.assertEqual(lead.email_from, partner_email)
+ self.assertEqual(lead.phone, partner_phone)
+
+ # writing on the lead field must change the partner field
+ lead.email_from = '"John Zoidberg" <john.zoidberg@test.example.com>'
+ lead.phone = '+1 202 555 7799'
+ self.assertEqual(partner.email, '"John Zoidberg" <john.zoidberg@test.example.com>')
+ self.assertEqual(partner.email_normalized, 'john.zoidberg@test.example.com')
+ self.assertEqual(partner.phone, '+1 202 555 7799')
+
+ # writing on the partner must change the lead values
+ partner.email = partner_email
+ partner.phone = '+1 202 555 6666'
+ self.assertEqual(lead.email_from, partner_email)
+ self.assertEqual(lead.phone, '+1 202 555 6666')
+
+ # resetting lead values also resets partner
+ lead.email_from, lead.phone = False, False
+ self.assertFalse(partner.email)
+ self.assertFalse(partner.email_normalized)
+ self.assertFalse(partner.phone)
+
+ @users('user_sales_manager')
+ def test_crm_lead_partner_sync_email_phone(self):
+ """ Specifically test synchronize between a lead and its partner about
+ phone and email fields. Phone especially has some corner cases due to
+ automatic formatting (notably with onchange in form view). """
+ lead, partner = self.lead_1.with_user(self.env.user), self.contact_2
+ lead_form = Form(lead)
+
+ # reset partner phone to a local number and prepare formatted / sanitized values
+ partner_phone, partner_mobile = self.test_phone_data[2], self.test_phone_data[1]
+ partner_phone_formatted = phone_format(partner_phone, 'US', '1')
+ partner_phone_sanitized = phone_format(partner_phone, 'US', '1', force_format='E164')
+ partner_mobile_formatted = phone_format(partner_mobile, 'US', '1')
+ partner_mobile_sanitized = phone_format(partner_mobile, 'US', '1', force_format='E164')
+ partner_email, partner_email_normalized = self.test_email_data[2], self.test_email_data_normalized[2]
+ self.assertEqual(partner_phone_formatted, '+1 202-555-0888')
+ self.assertEqual(partner_phone_sanitized, self.test_phone_data_sanitized[2])
+ self.assertEqual(partner_mobile_formatted, '+1 202-555-0999')
+ self.assertEqual(partner_mobile_sanitized, self.test_phone_data_sanitized[1])
+ # ensure initial data
+ self.assertEqual(partner.phone, partner_phone)
+ self.assertEqual(partner.mobile, partner_mobile)
+ self.assertEqual(partner.email, partner_email)
+
+ # LEAD/PARTNER SYNC: email and phone are propagated to lead
+ # as well as mobile (who does not trigger the reverse sync)
+ lead_form.partner_id = partner
+ self.assertEqual(lead_form.email_from, partner_email)
+ self.assertEqual(lead_form.phone, partner_phone_formatted,
+ 'Lead: form automatically formats numbers')
+ self.assertEqual(lead_form.mobile, partner_mobile_formatted,
+ 'Lead: form automatically formats numbers')
+ self.assertFalse(lead_form.ribbon_message)
+
+ lead_form.save()
+ self.assertEqual(partner.phone, partner_phone,
+ 'Lead / Partner: partner values sent to lead')
+ self.assertEqual(lead.email_from, partner_email,
+ 'Lead / Partner: partner values sent to lead')
+ self.assertEqual(lead.email_normalized, partner_email_normalized,
+ 'Lead / Partner: equal emails should lead to equal normalized emails')
+ self.assertEqual(lead.phone, partner_phone_formatted,
+ 'Lead / Partner: partner values (formatted) sent to lead')
+ self.assertEqual(lead.mobile, partner_mobile_formatted,
+ 'Lead / Partner: partner values (formatted) sent to lead')
+ self.assertEqual(lead.phone_sanitized, partner_mobile_sanitized,
+ 'Lead: phone_sanitized computed field on mobile')
+
+ # for email_from, if only formatting differs, warning ribbon should
+ # not appear and email on partner should not be updated
+ lead_form.email_from = '"Hermes Conrad" <%s>' % partner_email_normalized
+ self.assertFalse(lead_form.ribbon_message)
+ lead_form.save()
+ self.assertEqual(lead_form.partner_id.email, partner_email)
+
+ # LEAD/PARTNER SYNC: lead updates partner
+ new_email = '"John Zoidberg" <john.zoidberg@test.example.com>'
+ new_email_normalized = 'john.zoidberg@test.example.com'
+ lead_form.email_from = new_email
+ self.assertIn('the customer email will', lead_form.ribbon_message)
+ new_phone = '+1 202 555 7799'
+ new_phone_formatted = phone_format(new_phone, 'US', '1')
+ lead_form.phone = new_phone
+ self.assertEqual(lead_form.phone, new_phone_formatted)
+ self.assertIn('the customer email and phone number will', lead_form.ribbon_message)
+
+ lead_form.save()
+ self.assertEqual(partner.email, new_email)
+ self.assertEqual(partner.email_normalized, new_email_normalized)
+ self.assertEqual(partner.phone, new_phone_formatted)
+
+ # LEAD/PARTNER SYNC: mobile does not update partner
+ new_mobile = '+1 202 555 6543'
+ new_mobile_formatted = phone_format(new_mobile, 'US', '1')
+ lead_form.mobile = new_mobile
+ lead_form.save()
+ self.assertEqual(lead.mobile, new_mobile_formatted)
+ self.assertEqual(partner.mobile, partner_mobile)
+
+ # LEAD/PARTNER SYNC: reseting lead values also resets partner for email
+ # and phone, but not for mobile
+ lead_form.email_from, lead_form.phone, lead.mobile = False, False, False
+ self.assertIn('the customer email and phone number will', lead_form.ribbon_message)
+ lead_form.save()
+ self.assertFalse(partner.email)
+ self.assertFalse(partner.email_normalized)
+ self.assertFalse(partner.phone)
+ self.assertFalse(lead.phone)
+ self.assertFalse(lead.mobile)
+ self.assertFalse(lead.phone_sanitized)
+ self.assertEqual(partner.mobile, partner_mobile)
+ self.assertEqual(partner.phone_sanitized, partner_mobile_sanitized,
+ 'Partner sanitized should be computed on mobile')
+
+ @users('user_sales_manager')
+ def test_crm_lead_partner_sync_email_phone_corner_cases(self):
+ """ Test corner cases of email and phone sync (False versus '', formatting
+ differences, wrong input, ...) """
+ test_email = 'amy.wong@test.example.com'
+ lead = self.lead_1.with_user(self.env.user)
+ contact = self.env['res.partner'].create({
+ 'name': 'NoContact Partner',
+ 'phone': '',
+ 'email': '',
+ 'mobile': '',
+ })
+
+ lead_form = Form(lead)
+ self.assertEqual(lead_form.email_from, test_email)
+ self.assertFalse(lead_form.ribbon_message)
+
+ # email: False versus empty string
+ lead_form.partner_id = contact
+ self.assertIn('the customer email', lead_form.ribbon_message)
+ lead_form.email_from = ''
+ self.assertFalse(lead_form.ribbon_message)
+ lead_form.email_from = False
+ self.assertFalse(lead_form.ribbon_message)
+
+ # phone: False versus empty string
+ lead_form.phone = '+1 202-555-0888'
+ self.assertIn('the customer phone', lead_form.ribbon_message)
+ lead_form.phone = ''
+ self.assertFalse(lead_form.ribbon_message)
+ lead_form.phone = False
+ self.assertFalse(lead_form.ribbon_message)
+
+ # email/phone: formatting should not trigger ribbon
+ lead.write({
+ 'email_from': '"My Name" <%s>' % test_email,
+ 'phone': '+1 202-555-0888',
+ })
+ contact.write({
+ 'email': '"My Name" <%s>' % test_email,
+ 'phone': '+1 202-555-0888',
+ })
+
+ lead_form = Form(lead)
+ self.assertFalse(lead_form.ribbon_message)
+ lead_form.partner_id = contact
+ self.assertFalse(lead_form.ribbon_message)
+ lead_form.email_from = '"Another Name" <%s>' % test_email # same email normalized
+ self.assertFalse(lead_form.ribbon_message, 'Formatting-only change should not trigger write')
+ lead_form.phone = '2025550888' # same number but another format
+ self.assertFalse(lead_form.ribbon_message, 'Formatting-only change should not trigger write')
+
+ # wrong value are also propagated
+ lead_form.phone = '666 789456789456789456'
+ self.assertIn('the customer phone', lead_form.ribbon_message)
+
+ @users('user_sales_manager')
+ def test_crm_lead_stages(self):
+ lead = self.lead_1.with_user(self.env.user)
+ self.assertEqual(lead.team_id, self.sales_team_1)
+
+ lead.convert_opportunity(self.contact_1.id)
+ self.assertEqual(lead.team_id, self.sales_team_1)
+
+ lead.action_set_won()
+ self.assertEqual(lead.probability, 100.0)
+ self.assertEqual(lead.stage_id, self.stage_gen_won) # generic won stage has lower sequence than team won stage
+
+ @users('user_sales_leads')
+ def test_crm_lead_update_contact(self):
+ # ensure initial data, especially for corner cases
+ self.assertFalse(self.contact_company_1.phone)
+ self.assertEqual(self.contact_company_1.country_id.code, "US")
+ lead = self.env['crm.lead'].create({
+ 'name': 'Test',
+ 'country_id': self.country_ref.id,
+ 'email_from': self.test_email,
+ 'phone': self.test_phone,
+ })
+ self.assertEqual(lead.country_id, self.country_ref, "Country should be set on the lead")
+ lead.partner_id = False
+ self.assertEqual(lead.country_id, self.country_ref, "Country should still be set on the lead")
+ self.assertEqual(lead.email_from, self.test_email)
+ self.assertEqual(lead.phone, self.test_phone)
+ self.assertEqual(lead.email_state, 'correct')
+ self.assertEqual(lead.phone_state, 'correct')
+
+ lead.partner_id = self.contact_company_1
+ self.assertEqual(lead.country_id, self.contact_company_1.country_id, "Country should still be the one set on partner")
+ self.assertEqual(lead.email_from, self.contact_company_1.email)
+ self.assertEqual(lead.phone, self.test_phone)
+ self.assertEqual(lead.email_state, 'correct')
+ # currently we keep phone as partner as a void one -> may lead to inconsistencies
+ self.assertEqual(lead.phone_state, 'incorrect', "Belgian phone with US country -> considered as incorrect")
+
+ lead.email_from = 'broken'
+ lead.phone = 'alsobroken'
+ self.assertEqual(lead.email_state, 'incorrect')
+ self.assertEqual(lead.phone_state, 'incorrect')
+ self.assertEqual(self.contact_company_1.email, 'broken')
+ self.assertEqual(self.contact_company_1.phone, 'alsobroken')
+
+ @users('user_sales_manager')
+ def test_crm_team_alias(self):
+ new_team = self.env['crm.team'].create({
+ 'name': 'TestAlias',
+ 'use_leads': True,
+ 'use_opportunities': True,
+ 'alias_name': 'test.alias'
+ })
+ self.assertEqual(new_team.alias_id.alias_name, 'test.alias')
+ self.assertEqual(new_team.alias_name, 'test.alias')
+
+ new_team.write({
+ 'use_leads': False,
+ 'use_opportunities': False,
+ })
+ # self.assertFalse(new_team.alias_id.alias_name)
+ # self.assertFalse(new_team.alias_name)
+
+ def test_mailgateway(self):
+ new_lead = self.format_and_process(
+ INCOMING_EMAIL,
+ 'unknown.sender@test.example.com',
+ '%s@%s' % (self.sales_team_1.alias_name, self.alias_domain),
+ subject='Delivery cost inquiry',
+ target_model='crm.lead',
+ )
+ self.assertEqual(new_lead.email_from, 'unknown.sender@test.example.com')
+ self.assertFalse(new_lead.partner_id)
+ self.assertEqual(new_lead.name, 'Delivery cost inquiry')
+
+ message = new_lead.with_user(self.user_sales_manager).message_post(
+ body='Here is my offer !',
+ subtype_xmlid='mail.mt_comment')
+ self.assertEqual(message.author_id, self.user_sales_manager.partner_id)
+
+ new_lead.handle_partner_assignment(create_missing=True)
+ self.assertEqual(new_lead.partner_id.email, 'unknown.sender@test.example.com')
+ self.assertEqual(new_lead.partner_id.team_id, self.sales_team_1)
+
+ @users('user_sales_manager')
+ def test_phone_mobile_update(self):
+ lead = self.env['crm.lead'].create({
+ 'name': 'Lead 1',
+ 'country_id': self.env.ref('base.us').id,
+ 'phone': self.test_phone_data[0],
+ })
+ self.assertEqual(lead.phone, self.test_phone_data[0])
+ self.assertFalse(lead.mobile)
+ self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[0])
+
+ lead.write({'phone': False, 'mobile': self.test_phone_data[1]})
+ self.assertFalse(lead.phone)
+ self.assertEqual(lead.mobile, self.test_phone_data[1])
+ self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[1])
+
+ lead.write({'phone': self.test_phone_data[1], 'mobile': self.test_phone_data[2]})
+ self.assertEqual(lead.phone, self.test_phone_data[1])
+ self.assertEqual(lead.mobile, self.test_phone_data[2])
+ self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[2])
+
+ # updating country should trigger sanitize computation
+ lead.write({'country_id': self.env.ref('base.be').id})
+ self.assertEqual(lead.phone, self.test_phone_data[1])
+ self.assertEqual(lead.mobile, self.test_phone_data[2])
+ self.assertFalse(lead.phone_sanitized)
+
+ @users('user_sales_manager')
+ def test_phone_mobile_search(self):
+ lead_1 = self.env['crm.lead'].create({
+ 'name': 'Lead 1',
+ 'country_id': self.env.ref('base.be').id,
+ 'phone': '+32485001122',
+ })
+ _lead_2 = self.env['crm.lead'].create({
+ 'name': 'Lead 2',
+ 'country_id': self.env.ref('base.be').id,
+ 'phone': '+32485112233',
+ })
+ self.assertEqual(lead_1, self.env['crm.lead'].search([
+ ('phone_mobile_search', 'like', '+32485001122')
+ ]))
diff --git a/addons/crm/tests/test_crm_lead_convert.py b/addons/crm/tests/test_crm_lead_convert.py
new file mode 100644
index 00000000..d20a616d
--- /dev/null
+++ b/addons/crm/tests/test_crm_lead_convert.py
@@ -0,0 +1,484 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import SUPERUSER_ID
+from odoo.addons.crm.tests import common as crm_common
+from odoo.fields import Datetime
+from odoo.tests.common import tagged, users
+from odoo.tests.common import Form
+
+@tagged('lead_manage')
+class TestLeadConvertForm(crm_common.TestLeadConvertCommon):
+
+ @users('user_sales_manager')
+ def test_form_action_default(self):
+ """ Test Lead._find_matching_partner() """
+ lead = self.env['crm.lead'].browse(self.lead_1.ids)
+ customer = self.env['res.partner'].create({
+ "name": "Amy Wong",
+ "email": '"Amy, PhD Student, Wong" Tiny <AMY.WONG@test.example.com>'
+ })
+
+ wizard = Form(self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': lead.id,
+ 'active_ids': lead.ids,
+ }))
+
+ self.assertEqual(wizard.name, 'convert')
+ self.assertEqual(wizard.action, 'exist')
+ self.assertEqual(wizard.partner_id, customer)
+
+ @users('user_sales_manager')
+ def test_form_name_onchange(self):
+ """ Test Lead._find_matching_partner() """
+ lead = self.env['crm.lead'].browse(self.lead_1.ids)
+ lead_dup = lead.copy({'name': 'Duplicate'})
+ customer = self.env['res.partner'].create({
+ "name": "Amy Wong",
+ "email": '"Amy, PhD Student, Wong" Tiny <AMY.WONG@test.example.com>'
+ })
+
+ wizard = Form(self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': lead.id,
+ 'active_ids': lead.ids,
+ }))
+
+ self.assertEqual(wizard.name, 'merge')
+ self.assertEqual(wizard.action, 'exist')
+ self.assertEqual(wizard.partner_id, customer)
+ self.assertEqual(wizard.duplicated_lead_ids[:], lead + lead_dup)
+
+ wizard.name = 'convert'
+ wizard.action = 'create'
+ self.assertEqual(wizard.action, 'create', 'Should keep user input')
+ self.assertEqual(wizard.name, 'convert', 'Should keep user input')
+
+
+@tagged('lead_manage')
+class TestLeadConvert(crm_common.TestLeadConvertCommon):
+ """
+ TODO: created partner (handle assignation) has team of lead
+ TODO: create partner has user_id coming from wizard
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestLeadConvert, cls).setUpClass()
+ date = Datetime.from_string('2020-01-20 16:00:00')
+ cls.crm_lead_dt_mock.now.return_value = date
+
+ def test_initial_data(self):
+ """ Ensure initial data to avoid spaghetti test update afterwards """
+ self.assertFalse(self.lead_1.date_conversion)
+ self.assertEqual(self.lead_1.date_open, Datetime.from_string('2020-01-15 11:30:00'))
+ self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
+ self.assertEqual(self.lead_1.team_id, self.sales_team_1)
+ self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
+
+ @users('user_sales_manager')
+ def test_lead_convert_base(self):
+ """ Test base method ``convert_opportunity`` or crm.lead model """
+ self.contact_2.phone = False # force Falsy to compare with mobile
+ self.assertFalse(self.contact_2.phone)
+ lead = self.lead_1.with_user(self.env.user)
+ lead.write({
+ 'phone': '123456789',
+ })
+ self.assertEqual(lead.team_id, self.sales_team_1)
+ self.assertEqual(lead.stage_id, self.stage_team1_1)
+ self.assertEqual(lead.email_from, 'amy.wong@test.example.com')
+ lead.convert_opportunity(self.contact_2.id)
+
+ self.assertEqual(lead.type, 'opportunity')
+ self.assertEqual(lead.partner_id, self.contact_2)
+ self.assertEqual(lead.email_from, self.contact_2.email)
+ self.assertEqual(lead.mobile, self.contact_2.mobile)
+ self.assertEqual(lead.phone, '123456789')
+ self.assertEqual(lead.team_id, self.sales_team_1)
+ self.assertEqual(lead.stage_id, self.stage_team1_1)
+
+ @users('user_sales_manager')
+ def test_lead_convert_base_corner_cases(self):
+ """ Test base method ``convert_opportunity`` or crm.lead model with corner
+ cases: inactive, won, stage update, ... """
+ # inactive leads are not converted
+ lead = self.lead_1.with_user(self.env.user)
+ lead.action_archive()
+ self.assertFalse(lead.active)
+ lead.convert_opportunity(self.contact_2.id)
+
+ self.assertEqual(lead.type, 'lead')
+ self.assertEqual(lead.partner_id, self.env['res.partner'])
+
+ lead.action_unarchive()
+ self.assertTrue(lead.active)
+
+ # won leads are not converted
+ lead.action_set_won()
+ # TDE FIXME: set won does not take into account sales team when fetching a won stage
+ # self.assertEqual(lead.stage_id, self.stage_team1_won)
+ self.assertEqual(lead.stage_id, self.stage_gen_won)
+ self.assertEqual(lead.probability, 100)
+
+ lead.convert_opportunity(self.contact_2.id)
+ self.assertEqual(lead.type, 'lead')
+ self.assertEqual(lead.partner_id, self.env['res.partner'])
+
+ @users('user_sales_manager')
+ def test_lead_convert_base_w_salesmen(self):
+ """ Test base method ``convert_opportunity`` while forcing salesmen, as it
+ should also force sales team """
+ lead = self.lead_1.with_user(self.env.user)
+ self.assertEqual(lead.team_id, self.sales_team_1)
+ lead.convert_opportunity(False, user_ids=self.user_sales_salesman.ids)
+ self.assertEqual(lead.user_id, self.user_sales_salesman)
+ self.assertEqual(lead.team_id, self.sales_team_convert)
+ # TDE FIXME: convert does not recompute stage based on updated team of assigned user
+ # self.assertEqual(lead.stage_id, self.stage_team_convert_1)
+
+ @users('user_sales_manager')
+ def test_lead_convert_base_w_team(self):
+ """ Test base method ``convert_opportunity`` while forcing team """
+ lead = self.lead_1.with_user(self.env.user)
+ lead.convert_opportunity(False, team_id=self.sales_team_convert.id)
+ self.assertEqual(lead.team_id, self.sales_team_convert)
+ self.assertEqual(lead.user_id, self.user_sales_leads)
+ # TDE FIXME: convert does not recompute stage based on team
+ # self.assertEqual(lead.stage_id, self.stage_team_convert_1)
+
+ @users('user_sales_manager')
+ def test_lead_convert_corner_cases_crud(self):
+ """ Test Lead._find_matching_partner() """
+ # email formatting
+ other_lead = self.lead_1.copy()
+ other_lead.write({'partner_id': self.contact_1.id})
+
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'default_lead_id': other_lead.id,
+ }).create({})
+ self.assertEqual(convert.lead_id, other_lead)
+ self.assertEqual(convert.partner_id, self.contact_1)
+ self.assertEqual(convert.action, 'exist')
+
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'default_lead_id': other_lead.id,
+ 'active_model': 'crm.lead',
+ 'active_id': self.lead_1.id,
+ }).create({})
+ self.assertEqual(convert.lead_id, other_lead)
+ self.assertEqual(convert.partner_id, self.contact_1)
+ self.assertEqual(convert.action, 'exist')
+
+ @users('user_sales_manager')
+ def test_lead_convert_corner_cases_matching(self):
+ """ Test Lead._find_matching_partner() """
+ # email formatting
+ self.lead_1.write({
+ 'email_from': 'Amy Wong <amy.wong@test.example.com>'
+ })
+ customer = self.env['res.partner'].create({
+ 'name': 'Different Name',
+ 'email': 'Wong AMY <AMY.WONG@test.example.com>'
+ })
+
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': self.lead_1.id,
+ 'active_ids': self.lead_1.ids,
+ }).create({})
+ # TDE FIXME: should take into account normalized email version, not encoded one
+ # self.assertEqual(convert.partner_id, customer)
+
+ @users('user_sales_manager')
+ def test_lead_convert_internals(self):
+ """ Test internals of convert wizard """
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': self.lead_1.id,
+ 'active_ids': self.lead_1.ids,
+ }).create({})
+
+ # test internals of convert wizard
+ self.assertEqual(convert.lead_id, self.lead_1)
+ self.assertEqual(convert.user_id, self.lead_1.user_id)
+ self.assertEqual(convert.team_id, self.lead_1.team_id)
+ self.assertFalse(convert.partner_id)
+ self.assertEqual(convert.name, 'convert')
+ self.assertEqual(convert.action, 'create')
+
+ convert.write({'user_id': self.user_sales_salesman.id})
+ self.assertEqual(convert.user_id, self.user_sales_salesman)
+ self.assertEqual(convert.team_id, self.sales_team_convert)
+
+ convert.action_apply()
+ # convert test
+ self.assertEqual(self.lead_1.type, 'opportunity')
+ self.assertEqual(self.lead_1.user_id, self.user_sales_salesman)
+ self.assertEqual(self.lead_1.team_id, self.sales_team_convert)
+ # TDE FIXME: stage is linked to the old sales team and is not updated when converting, could be improved
+ # self.assertEqual(self.lead_1.stage_id, self.stage_gen_1)
+ # partner creation test
+ new_partner = self.lead_1.partner_id
+ self.assertEqual(new_partner.name, 'Amy Wong')
+ self.assertEqual(new_partner.email, 'amy.wong@test.example.com')
+
+ @users('user_sales_manager')
+ def test_lead_convert_action_exist(self):
+ """ Test specific use case of 'exist' action in conver wizard """
+ self.lead_1.write({'partner_id': self.contact_1.id})
+
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': self.lead_1.id,
+ 'active_ids': self.lead_1.ids,
+ }).create({})
+ self.assertEqual(convert.action, 'exist')
+ convert.action_apply()
+ self.assertEqual(self.lead_1.type, 'opportunity')
+ self.assertEqual(self.lead_1.partner_id, self.contact_1)
+
+ @users('user_sales_manager')
+ def test_lead_convert_action_nothing(self):
+ """ Test specific use case of 'nothing' action in conver wizard """
+ self.lead_1.write({'contact_name': False})
+
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': self.lead_1.id,
+ 'active_ids': self.lead_1.ids,
+ }).create({})
+ self.assertEqual(convert.action, 'nothing')
+ convert.action_apply()
+ self.assertEqual(self.lead_1.type, 'opportunity')
+ self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
+ self.assertEqual(self.lead_1.team_id, self.sales_team_1)
+ self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
+ self.assertEqual(self.lead_1.partner_id, self.env['res.partner'])
+
+ @users('user_sales_manager')
+ def test_lead_convert_contact_mutlicompany(self):
+ """ Check the wizard convert to opp don't find contact
+ You are not able to see because they belong to another company """
+ # Use superuser_id because creating a company with a user add directly
+ # the company in company_ids of the user.
+ company_2 = self.env['res.company'].with_user(SUPERUSER_ID).create({'name': 'Company 2'})
+ partner_company_2 = self.env['res.partner'].with_user(SUPERUSER_ID).create({
+ 'name': 'Contact in other company',
+ 'email': 'test@company2.com',
+ 'company_id': company_2.id,
+ })
+ lead = self.env['crm.lead'].create({
+ 'name': 'LEAD',
+ 'type': 'lead',
+ 'email_from': 'test@company2.com',
+ })
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': lead.id,
+ 'active_ids': lead.ids,
+ }).create({'name': 'convert', 'action': 'exist'})
+ self.assertNotEqual(convert.partner_id, partner_company_2,
+ "Conversion wizard should not be able to find the partner from another company")
+
+ @users('user_sales_manager')
+ def test_lead_convert_same_partner(self):
+ """ Check that we don't erase lead information
+ with existing partner info if the partner is already set
+ """
+ partner = self.env['res.partner'].create({
+ 'name': 'Empty partner',
+ })
+ lead = self.env['crm.lead'].create({
+ 'name': 'LEAD',
+ 'partner_id': partner.id,
+ 'type': 'lead',
+ 'email_from': 'demo@test.com',
+ 'street': 'my street',
+ 'city': 'my city',
+ })
+ lead.convert_opportunity(partner.id)
+ self.assertEqual(lead.email_from, 'demo@test.com', 'Email From should be preserved during conversion')
+ self.assertEqual(lead.street, 'my street', 'Street should be preserved during conversion')
+ self.assertEqual(lead.city, 'my city', 'City should be preserved during conversion')
+
+ @users('user_sales_manager')
+ def test_lead_merge(self):
+ """ Test convert wizard working in merge mode """
+ date = Datetime.from_string('2020-01-20 16:00:00')
+ self.crm_lead_dt_mock.now.return_value = date
+
+ leads = self.env['crm.lead']
+ for x in range(2):
+ leads |= self.env['crm.lead'].create({
+ 'name': 'Dup-%02d-%s' % (x+1, self.lead_1.name),
+ 'type': 'lead', 'user_id': False, 'team_id': self.lead_1.team_id.id,
+ 'contact_name': 'Duplicate %02d of %s' % (x+1, self.lead_1.contact_name),
+ 'email_from': self.lead_1.email_from,
+ })
+
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': self.lead_1.id,
+ 'active_ids': self.lead_1.ids,
+ }).create({})
+
+ # test internals of convert wizard
+ self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | leads)
+ self.assertEqual(convert.user_id, self.lead_1.user_id)
+ self.assertEqual(convert.team_id, self.lead_1.team_id)
+ self.assertFalse(convert.partner_id)
+ self.assertEqual(convert.name, 'merge')
+ self.assertEqual(convert.action, 'create')
+
+ convert.write({'user_id': self.user_sales_salesman.id})
+ self.assertEqual(convert.user_id, self.user_sales_salesman)
+ self.assertEqual(convert.team_id, self.sales_team_convert)
+
+ convert.action_apply()
+ self.assertEqual(self.lead_1.type, 'opportunity')
+
+ @users('user_sales_manager')
+ def test_lead_merge_duplicates(self):
+ """ Test Lead._get_lead_duplicates() """
+
+ # Check: partner / email fallbacks
+ self._create_duplicates(self.lead_1)
+ self.lead_1.write({
+ 'partner_id': self.customer.id,
+ })
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': self.lead_1.id,
+ 'active_ids': self.lead_1.ids,
+ }).create({})
+ self.assertEqual(convert.partner_id, self.customer)
+ # self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | self.lead_email_from | self.lead_email_normalized | self.lead_partner | self.opp_lost)
+ self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | self.lead_email_from | self.lead_partner | self.opp_lost)
+
+ # Check: partner fallbacks
+ self.lead_1.write({
+ 'email_from': False,
+ 'partner_id': self.customer.id,
+ })
+ self.customer.write({'email': False})
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': self.lead_1.id,
+ 'active_ids': self.lead_1.ids,
+ }).create({})
+ self.assertEqual(convert.partner_id, self.customer)
+ self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | self.lead_partner)
+
+ @users('user_sales_manager')
+ def test_lead_merge_duplicates_flow(self):
+ """ Test Lead._get_lead_duplicates() + merge with active_test """
+
+ # Check: email formatting
+ self.lead_1.write({
+ 'email_from': 'Amy Wong <amy.wong@test.example.com>'
+ })
+ self._create_duplicates(self.lead_1)
+
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': self.lead_1.id,
+ 'active_ids': self.lead_1.ids,
+ }).create({})
+ self.assertEqual(convert.partner_id, self.customer)
+ # TDE FIXME: should check for email_normalized -> lead_email_normalized not correctly found
+ # self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | lead_email_from | lead_email_normalized | lead_partner | opp_lost)
+ self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | self.lead_email_from | self.lead_partner | self.opp_lost)
+
+ convert.action_apply()
+ self.assertEqual(
+ # (self.lead_1 | self.lead_email_from | self.lead_email_normalized | self.lead_partner | self.opp_lost).exists(),
+ (self.lead_1 | self.lead_email_from | self.lead_partner | self.opp_lost).exists(),
+ self.opp_lost)
+
+
+@tagged('lead_manage')
+class TestLeadConvertBatch(crm_common.TestLeadConvertMassCommon):
+
+ def test_initial_data(self):
+ """ Ensure initial data to avoid spaghetti test update afterwards """
+ self.assertFalse(self.lead_1.date_conversion)
+ self.assertEqual(self.lead_1.date_open, Datetime.from_string('2020-01-15 11:30:00'))
+ self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
+ self.assertEqual(self.lead_1.team_id, self.sales_team_1)
+ self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
+
+ self.assertEqual(self.lead_w_partner.stage_id, self.env['crm.stage'])
+ self.assertEqual(self.lead_w_partner.user_id, self.user_sales_manager)
+ self.assertEqual(self.lead_w_partner.team_id, self.sales_team_1)
+
+ self.assertEqual(self.lead_w_partner_company.stage_id, self.stage_team1_1)
+ self.assertEqual(self.lead_w_partner_company.user_id, self.user_sales_manager)
+ self.assertEqual(self.lead_w_partner_company.team_id, self.sales_team_1)
+
+ self.assertEqual(self.lead_w_contact.stage_id, self.stage_gen_1)
+ self.assertEqual(self.lead_w_contact.user_id, self.user_sales_salesman)
+ self.assertEqual(self.lead_w_contact.team_id, self.sales_team_convert)
+
+ self.assertEqual(self.lead_w_email.stage_id, self.stage_gen_1)
+ self.assertEqual(self.lead_w_email.user_id, self.user_sales_salesman)
+ self.assertEqual(self.lead_w_email.team_id, self.sales_team_convert)
+
+ self.assertEqual(self.lead_w_email_lost.stage_id, self.stage_team1_2)
+ self.assertEqual(self.lead_w_email_lost.user_id, self.user_sales_leads)
+ self.assertEqual(self.lead_w_email_lost.team_id, self.sales_team_1)
+
+ @users('user_sales_manager')
+ def test_lead_convert_batch_internals(self):
+ """ Test internals of convert wizard, working in batch mode """
+ date = Datetime.from_string('2020-01-20 16:00:00')
+ self.crm_lead_dt_mock.now.return_value = date
+
+ lead_w_partner = self.lead_w_partner
+ lead_w_contact = self.lead_w_contact
+ lead_w_email_lost = self.lead_w_email_lost
+ lead_w_email_lost.action_set_lost()
+ self.assertEqual(lead_w_email_lost.active, False)
+
+ convert = self.env['crm.lead2opportunity.partner'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_id': self.lead_1.id,
+ 'active_ids': (self.lead_1 | lead_w_partner | lead_w_contact | lead_w_email_lost).ids,
+ }).create({})
+
+ # test internals of convert wizard
+ # self.assertEqual(convert.lead_id, self.lead_1)
+ self.assertEqual(convert.user_id, self.lead_1.user_id)
+ self.assertEqual(convert.team_id, self.lead_1.team_id)
+ self.assertFalse(convert.partner_id)
+ self.assertEqual(convert.name, 'convert')
+ self.assertEqual(convert.action, 'create')
+
+ convert.action_apply()
+ self.assertEqual(convert.user_id, self.user_sales_leads)
+ self.assertEqual(convert.team_id, self.sales_team_1)
+ # lost leads are not converted (see crm_lead.convert_opportunity())
+ self.assertFalse(lead_w_email_lost.active)
+ self.assertFalse(lead_w_email_lost.date_conversion)
+ self.assertEqual(lead_w_email_lost.partner_id, self.env['res.partner'])
+ self.assertEqual(lead_w_email_lost.stage_id, self.stage_team1_2) # did not change
+ # other leads are converted into opportunities
+ for opp in (self.lead_1 | lead_w_partner | lead_w_contact):
+ # team management update: opportunity linked to chosen wizard values
+ self.assertEqual(opp.type, 'opportunity')
+ self.assertTrue(opp.active)
+ self.assertEqual(opp.user_id, convert.user_id)
+ self.assertEqual(opp.team_id, convert.team_id)
+ # dates update: convert set them to now
+ self.assertEqual(opp.date_open, date)
+ self.assertEqual(opp.date_conversion, date)
+ # stage update (depends on previous value)
+ if opp == self.lead_1:
+ self.assertEqual(opp.stage_id, self.stage_team1_1) # did not change
+ elif opp == lead_w_partner:
+ self.assertEqual(opp.stage_id, self.stage_team1_1) # is set to default stage of sales_team_1
+ elif opp == lead_w_contact:
+ self.assertEqual(opp.stage_id, self.stage_gen_1) # did not change
+ else:
+ self.assertFalse(True)
diff --git a/addons/crm/tests/test_crm_lead_convert_mass.py b/addons/crm/tests/test_crm_lead_convert_mass.py
new file mode 100644
index 00000000..bee86106
--- /dev/null
+++ b/addons/crm/tests/test_crm_lead_convert_mass.py
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.crm.tests import common as crm_common
+from odoo.tests.common import tagged, users
+
+
+@tagged('lead_manage', 'crm_performance')
+class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestLeadConvertMass, cls).setUpClass()
+
+ cls.leads = cls.lead_1 + cls.lead_w_partner + cls.lead_w_email_lost
+ cls.assign_users = cls.user_sales_manager + cls.user_sales_leads_convert + cls.user_sales_salesman
+
+ @users('user_sales_manager')
+ def test_assignment_salesmen(self):
+ test_leads = self._create_leads_batch(count=50, user_ids=[False])
+ user_ids = self.assign_users.ids
+ self.assertEqual(test_leads.user_id, self.env['res.users'])
+
+ with self.assertQueryCount(user_sales_manager=0):
+ test_leads = self.env['crm.lead'].browse(test_leads.ids)
+
+ with self.assertQueryCount(user_sales_manager=254): # often 251, sometimes +3 on runbot
+ test_leads.handle_salesmen_assignment(user_ids=user_ids, team_id=False)
+
+ self.assertEqual(test_leads.team_id, self.sales_team_convert | self.sales_team_1)
+ self.assertEqual(test_leads[0::3].user_id, self.user_sales_manager)
+ self.assertEqual(test_leads[1::3].user_id, self.user_sales_leads_convert)
+ self.assertEqual(test_leads[2::3].user_id, self.user_sales_salesman)
+
+ @users('user_sales_manager')
+ def test_assignment_salesmen_wteam(self):
+ test_leads = self._create_leads_batch(count=50, user_ids=[False])
+ user_ids = self.assign_users.ids
+ team_id = self.sales_team_convert.id
+ self.assertEqual(test_leads.user_id, self.env['res.users'])
+
+ with self.assertQueryCount(user_sales_manager=0):
+ test_leads = self.env['crm.lead'].browse(test_leads.ids)
+
+ with self.assertQueryCount(user_sales_manager=221): # crm only: 215 - generally 218, sometimes +2/+3 on runbot
+ test_leads.handle_salesmen_assignment(user_ids=user_ids, team_id=team_id)
+
+ self.assertEqual(test_leads.team_id, self.sales_team_convert)
+ self.assertEqual(test_leads[0::3].user_id, self.user_sales_manager)
+ self.assertEqual(test_leads[1::3].user_id, self.user_sales_leads_convert)
+ self.assertEqual(test_leads[2::3].user_id, self.user_sales_salesman)
+
+ @users('user_sales_manager')
+ def test_mass_convert_internals(self):
+ """ Test internals mass converted in convert mode, without duplicate management """
+ # reset some assigned users to test salesmen assign
+ (self.lead_w_partner | self.lead_w_email_lost).write({
+ 'user_id': False
+ })
+
+ mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_ids': self.leads.ids,
+ 'active_id': self.leads.ids[0]
+ }).create({
+ 'deduplicate': False,
+ 'user_id': self.user_sales_salesman.id,
+ 'force_assignment': False,
+ })
+
+ # default values
+ self.assertEqual(mass_convert.name, 'convert')
+ self.assertEqual(mass_convert.action, 'each_exist_or_create')
+ # depending on options
+ self.assertEqual(mass_convert.partner_id, self.env['res.partner'])
+ self.assertEqual(mass_convert.deduplicate, False)
+ self.assertEqual(mass_convert.user_id, self.user_sales_salesman)
+ self.assertEqual(mass_convert.team_id, self.sales_team_convert)
+
+ mass_convert.action_mass_convert()
+ for lead in self.lead_1 | self.lead_w_partner:
+ self.assertEqual(lead.type, 'opportunity')
+ if lead == self.lead_w_partner:
+ self.assertEqual(lead.user_id, self.env['res.users']) # user_id is bypassed
+ self.assertEqual(lead.partner_id, self.contact_1)
+ elif lead == self.lead_1:
+ self.assertEqual(lead.user_id, self.user_sales_leads) # existing value not forced
+ new_partner = lead.partner_id
+ self.assertEqual(new_partner.name, 'Amy Wong')
+ self.assertEqual(new_partner.email, 'amy.wong@test.example.com')
+
+ # test unforced assignation
+ mass_convert.write({
+ 'user_ids': self.user_sales_salesman.ids,
+ })
+ mass_convert.action_mass_convert()
+ self.assertEqual(self.lead_w_partner.user_id, self.user_sales_salesman)
+ self.assertEqual(self.lead_1.user_id, self.user_sales_leads) # existing value not forced
+
+ # lost leads are untouched
+ self.assertEqual(self.lead_w_email_lost.type, 'lead')
+ self.assertFalse(self.lead_w_email_lost.active)
+ self.assertFalse(self.lead_w_email_lost.date_conversion)
+ # TDE FIXME: partner creation is done even on lost leads because not checked in wizard
+ # self.assertEqual(self.lead_w_email_lost.partner_id, self.env['res.partner'])
+
+ @users('user_sales_manager')
+ def test_mass_convert_deduplicate(self):
+ """ Test duplicated_lead_ids fields having another behavior in mass convert
+ because why not. Its use is: among leads under convert, store those with
+ duplicates if deduplicate is set to True. """
+ lead_1_dups = self._create_duplicates(self.lead_1, create_opp=False)
+ lead_1_final = self.lead_1 # after merge: same but with lower ID
+
+ lead_w_partner_dups = self._create_duplicates(self.lead_w_partner, create_opp=False)
+ lead_w_partner_final = lead_w_partner_dups[0] # lead_w_partner has no stage -> lower in sort by confidence
+ lead_w_partner_dups_partner = lead_w_partner_dups[1] # copy with a partner_id (with the same email)
+
+ mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_ids': self.leads.ids,
+ }).create({
+ 'deduplicate': True,
+ })
+ self.assertEqual(mass_convert.action, 'each_exist_or_create')
+ self.assertEqual(mass_convert.name, 'convert')
+ self.assertEqual(mass_convert.lead_tomerge_ids, self.leads)
+ self.assertEqual(mass_convert.duplicated_lead_ids, self.lead_1 | self.lead_w_partner)
+
+ mass_convert.action_mass_convert()
+
+ self.assertEqual(
+ (lead_1_dups | lead_w_partner_dups | lead_w_partner_dups_partner).exists(),
+ lead_w_partner_final
+ )
+ for lead in lead_1_final | lead_w_partner_final:
+ self.assertTrue(lead.active)
+ self.assertEqual(lead.type, 'opportunity')
+
+ @users('user_sales_manager')
+ def test_mass_convert_find_existing(self):
+ """ Check that we don't find a wrong partner
+ that have similar name during mass conversion
+ """
+ wrong_partner = self.env['res.partner'].create({
+ 'name': 'casa depapel',
+ 'street': "wrong street"
+ })
+
+ lead = self.env['crm.lead'].create({'name': 'Asa Depape'})
+ mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_ids': lead.ids,
+ 'active_id': lead.ids[0]
+ }).create({
+ 'deduplicate': False,
+ 'action': 'each_exist_or_create',
+ 'name': 'convert',
+ })
+ mass_convert.action_mass_convert()
+
+ self.assertNotEqual(lead.partner_id, wrong_partner, "Partner Id should not match the wrong contact")
+
+ @users('user_sales_manager')
+ def test_mass_convert_performances(self):
+ test_leads = self._create_leads_batch(count=50, user_ids=[False])
+ user_ids = self.assign_users.ids
+
+ with self.assertQueryCount(user_sales_manager=1361): # still some randomness (1360 spotted) - crm only: 1352
+ mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_ids': test_leads.ids,
+ }).create({
+ 'deduplicate': True,
+ 'user_ids': user_ids,
+ 'force_assignment': True,
+ })
+ mass_convert.action_mass_convert()
+
+ self.assertEqual(set(test_leads.mapped('type')), set(['opportunity']))
+ self.assertEqual(len(test_leads.partner_id), len(test_leads))
+ # TDE FIXME: strange
+ # self.assertEqual(test_leads.team_id, self.sales_team_convert | self.sales_team_1)
+ self.assertEqual(test_leads.team_id, self.sales_team_1)
+ self.assertEqual(test_leads[0::3].user_id, self.user_sales_manager)
+ self.assertEqual(test_leads[1::3].user_id, self.user_sales_leads_convert)
+ self.assertEqual(test_leads[2::3].user_id, self.user_sales_salesman)
+
+ @users('user_sales_manager')
+ def test_mass_convert_w_salesmen(self):
+ # reset some assigned users to test salesmen assign
+ (self.lead_w_partner | self.lead_w_email_lost).write({
+ 'user_id': False
+ })
+
+ mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_ids': self.leads.ids,
+ 'active_id': self.leads.ids[0]
+ }).create({
+ 'deduplicate': False,
+ 'user_ids': self.assign_users.ids,
+ 'force_assignment': True,
+ })
+
+ # TDE FIXME: what happens if we mix people from different sales team ? currently nothing, to check
+ mass_convert.action_mass_convert()
+
+ for idx, lead in enumerate(self.leads - self.lead_w_email_lost):
+ self.assertEqual(lead.type, 'opportunity')
+ assigned_user = self.assign_users[idx % len(self.assign_users)]
+ self.assertEqual(lead.user_id, assigned_user)
diff --git a/addons/crm/tests/test_crm_lead_lost.py b/addons/crm/tests/test_crm_lead_lost.py
new file mode 100644
index 00000000..7f2819f7
--- /dev/null
+++ b/addons/crm/tests/test_crm_lead_lost.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.crm.tests import common as crm_common
+from odoo.exceptions import AccessError
+from odoo.tests.common import tagged, users
+
+
+@tagged('lead_manage')
+class TestLeadConvert(crm_common.TestCrmCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestLeadConvert, cls).setUpClass()
+ cls.lost_reason = cls.env['crm.lost.reason'].create({
+ 'name': 'Test Reason'
+ })
+
+ @users('user_sales_salesman')
+ def test_lead_lost(self):
+ self.lead_1.with_user(self.user_sales_manager).write({
+ 'user_id': self.user_sales_salesman.id,
+ 'probability': 32,
+ })
+
+ lead = self.lead_1.with_user(self.env.user)
+ self.assertEqual(lead.probability, 32)
+
+ lost_wizard = self.env['crm.lead.lost'].with_context({
+ 'active_ids': lead.ids,
+ }).create({
+ 'lost_reason_id': self.lost_reason.id
+ })
+
+ lost_wizard.action_lost_reason_apply()
+
+ self.assertEqual(lead.probability, 0)
+ self.assertEqual(lead.automated_probability, 0)
+ self.assertFalse(lead.active)
+ self.assertEqual(lead.lost_reason, self.lost_reason) # TDE FIXME: should be called lost_reason_id non didjou
+
+ @users('user_sales_salesman')
+ def test_lead_lost_crm_rights(self):
+ lead = self.lead_1.with_user(self.env.user)
+
+ # nice try little salesman but only managers can create lost reason to avoid bloating the DB
+ with self.assertRaises(AccessError):
+ lost_reason = self.env['crm.lost.reason'].create({
+ 'name': 'Test Reason'
+ })
+
+ with self.with_user('user_sales_manager'):
+ lost_reason = self.env['crm.lost.reason'].create({
+ 'name': 'Test Reason'
+ })
+
+ lost_wizard = self.env['crm.lead.lost'].with_context({
+ 'active_ids': lead.ids
+ }).create({
+ 'lost_reason_id': lost_reason.id
+ })
+
+ # nice try little salesman, you cannot invoke a wizard to update other people leads
+ with self.assertRaises(AccessError):
+ lost_wizard.action_lost_reason_apply()
diff --git a/addons/crm/tests/test_crm_lead_merge.py b/addons/crm/tests/test_crm_lead_merge.py
new file mode 100644
index 00000000..1bcf1c2a
--- /dev/null
+++ b/addons/crm/tests/test_crm_lead_merge.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.crm.tests.common import TestLeadConvertMassCommon
+from odoo.fields import Datetime
+from odoo.tests.common import tagged, users
+
+
+@tagged('lead_manage')
+class TestLeadMerge(TestLeadConvertMassCommon):
+ """ During a mixed merge (involving leads and opps), data should be handled a certain way following their type
+ (m2o, m2m, text, ...). """
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestLeadMerge, cls).setUpClass()
+
+ cls.leads = cls.lead_1 + cls.lead_w_partner + cls.lead_w_contact + cls.lead_w_email + cls.lead_w_partner_company + cls.lead_w_email_lost
+ # reset some assigned users to test salesmen assign
+ (cls.lead_w_partner | cls.lead_w_email_lost).write({
+ 'user_id': False,
+ })
+ cls.lead_w_partner.write({'stage_id': False})
+
+ cls.lead_w_contact.write({'description': 'lead_w_contact'})
+ cls.lead_w_email.write({'description': 'lead_w_email'})
+ cls.lead_1.write({'description': 'lead_1'})
+ cls.lead_w_partner.write({'description': 'lead_w_partner'})
+
+ cls.assign_users = cls.user_sales_manager + cls.user_sales_leads_convert + cls.user_sales_salesman
+
+ def test_initial_data(self):
+ """ Ensure initial data to avoid spaghetti test update afterwards """
+ self.assertFalse(self.lead_1.date_conversion)
+ self.assertEqual(self.lead_1.date_open, Datetime.from_string('2020-01-15 11:30:00'))
+ self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
+ self.assertEqual(self.lead_1.team_id, self.sales_team_1)
+ self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
+
+ self.assertEqual(self.lead_w_partner.stage_id, self.env['crm.stage'])
+ self.assertEqual(self.lead_w_partner.user_id, self.env['res.users'])
+ self.assertEqual(self.lead_w_partner.team_id, self.sales_team_1)
+
+ self.assertEqual(self.lead_w_partner_company.stage_id, self.stage_team1_1)
+ self.assertEqual(self.lead_w_partner_company.user_id, self.user_sales_manager)
+ self.assertEqual(self.lead_w_partner_company.team_id, self.sales_team_1)
+
+ self.assertEqual(self.lead_w_contact.stage_id, self.stage_gen_1)
+ self.assertEqual(self.lead_w_contact.user_id, self.user_sales_salesman)
+ self.assertEqual(self.lead_w_contact.team_id, self.sales_team_convert)
+
+ self.assertEqual(self.lead_w_email.stage_id, self.stage_gen_1)
+ self.assertEqual(self.lead_w_email.user_id, self.user_sales_salesman)
+ self.assertEqual(self.lead_w_email.team_id, self.sales_team_convert)
+
+ self.assertEqual(self.lead_w_email_lost.stage_id, self.stage_team1_2)
+ self.assertEqual(self.lead_w_email_lost.user_id, self.env['res.users'])
+ self.assertEqual(self.lead_w_email_lost.team_id, self.sales_team_1)
+
+ @users('user_sales_manager')
+ def test_lead_merge_internals(self):
+ """ Test internals of merge wizard. In this test leads are ordered as
+
+ lead_w_contact --lead---seq=30
+ lead_w_email ----lead---seq=3
+ lead_1 ----------lead---seq=1
+ lead_w_partner --lead---seq=False
+ """
+ # ensure initial data
+ self.lead_w_partner_company.action_set_won() # won opps should be excluded
+
+ merge = self.env['crm.merge.opportunity'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_ids': self.leads.ids,
+ 'active_id': False,
+ }).create({
+ 'user_id': self.user_sales_leads_convert.id,
+ })
+ self.assertEqual(merge.team_id, self.sales_team_convert)
+
+ # TDE FIXME: not sure the browse in default get of wizard intended to exlude lost, as it browse ids
+ # and exclude inactive leads, but that's not written anywhere ... intended ??
+ self.assertEqual(merge.opportunity_ids, self.leads - self.lead_w_partner_company - self.lead_w_email_lost)
+ ordered_merge = self.lead_w_contact + self.lead_w_email + self.lead_1 + self.lead_w_partner
+ ordered_merge_description = '\n\n'.join(l.description for l in ordered_merge)
+
+ # merged opportunity: in this test, all input are leads. Confidence is based on stage
+ # sequence -> lead_w_contact has a stage sequence of 30
+ result = merge.action_merge()
+ merge_opportunity = self.env['crm.lead'].browse(result['res_id'])
+ self.assertFalse((ordered_merge - merge_opportunity).exists())
+ self.assertEqual(merge_opportunity, self.lead_w_contact)
+ self.assertEqual(merge_opportunity.type, 'lead')
+ self.assertEqual(merge_opportunity.description, ordered_merge_description)
+ # merged opportunity has updated salesman / team / stage is ok as generic
+ self.assertEqual(merge_opportunity.user_id, self.user_sales_leads_convert)
+ self.assertEqual(merge_opportunity.team_id, self.sales_team_convert)
+ self.assertEqual(merge_opportunity.stage_id, self.stage_gen_1)
+
+ @users('user_sales_manager')
+ def test_lead_merge_mixed(self):
+ """ In case of mix, opportunities are on top, and result is an opportunity
+
+ lead_1 -------------------opp----seq=1
+ lead_w_partner_company ---opp----seq=1 (ID greater)
+ lead_w_contact -----------lead---seq=30
+ lead_w_email -------------lead---seq=3
+ lead_w_partner -----------lead---seq=False
+ """
+ # ensure initial data
+ (self.lead_w_partner_company | self.lead_1).write({'type': 'opportunity'})
+ self.assertEqual(self.lead_w_partner_company.stage_id.sequence, 1)
+ self.assertEqual(self.lead_1.stage_id.sequence, 1)
+
+ merge = self.env['crm.merge.opportunity'].with_context({
+ 'active_model': 'crm.lead',
+ 'active_ids': self.leads.ids,
+ 'active_id': False,
+ }).create({
+ 'team_id': self.sales_team_convert.id,
+ 'user_id': False,
+ })
+ # TDE FIXME: see aa44700dccdc2618e0b8bc94252789264104047c -> no user, no team -> strange
+ merge.write({'team_id': self.sales_team_convert.id})
+
+ # TDE FIXME: not sure the browse in default get of wizard intended to exlude lost, as it browse ids
+ # and exclude inactive leads, but that's not written anywhere ... intended ??
+ self.assertEqual(merge.opportunity_ids, self.leads - self.lead_w_email_lost)
+ ordered_merge = self.lead_w_partner_company + self.lead_w_contact + self.lead_w_email + self.lead_w_partner
+
+ result = merge.action_merge()
+ merge_opportunity = self.env['crm.lead'].browse(result['res_id'])
+ self.assertFalse((ordered_merge - merge_opportunity).exists())
+ self.assertEqual(merge_opportunity, self.lead_1)
+ self.assertEqual(merge_opportunity.type, 'opportunity')
+
+ # merged opportunity has same salesman (not updated in wizard)
+ self.assertEqual(merge_opportunity.user_id, self.user_sales_leads)
+ # TDE FIXME: as same uer_id is enforced, team is updated through onchange and therefore stage
+ self.assertEqual(merge_opportunity.team_id, self.sales_team_convert)
+ # self.assertEqual(merge_opportunity.team_id, self.sales_team_1)
+ # TDE FIXME: BUT team_id is computed after checking stage, based on wizard's team_id
+ self.assertEqual(merge_opportunity.stage_id, self.stage_team_convert_1)
diff --git a/addons/crm/tests/test_crm_lead_notification.py b/addons/crm/tests/test_crm_lead_notification.py
new file mode 100644
index 00000000..2e6e646d
--- /dev/null
+++ b/addons/crm/tests/test_crm_lead_notification.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from .common import TestCrmCommon
+
+
+class NewLeadNotification(TestCrmCommon):
+
+ def test_new_lead_notification(self):
+ """ Test newly create leads like from the website. People and channels
+ subscribed to the Sales Team shoud be notified. """
+ # subscribe a partner and a channel to the Sales Team with new lead subtype
+ channel_listen = self.env['mail.channel'].create({'name': 'Listener'})
+ sales_team_1 = self.env['crm.team'].create({
+ 'name': 'Test Sales Team',
+ 'alias_name': 'test_sales_team',
+ })
+
+ subtype = self.env.ref("crm.mt_salesteam_lead")
+ sales_team_1.message_subscribe(partner_ids=[self.user_sales_manager.partner_id.id], channel_ids=[channel_listen.id], subtype_ids=[subtype.id])
+
+ # Imitate what happens in the controller when somebody creates a new
+ # lead from the website form
+ lead = self.env["crm.lead"].with_context(mail_create_nosubscribe=True).sudo().create({
+ "contact_name": "Somebody",
+ "description": "Some question",
+ "email_from": "somemail@example.com",
+ "name": "Some subject",
+ "partner_name": "Some company",
+ "team_id": sales_team_1.id,
+ "phone": "+0000000000"
+ })
+ # partner and channel should be auto subscribed
+ self.assertIn(self.user_sales_manager.partner_id, lead.message_partner_ids)
+ self.assertIn(channel_listen, lead.message_channel_ids)
+
+ msg = lead.message_ids[0]
+ self.assertIn(self.user_sales_manager.partner_id, msg.notified_partner_ids)
+ self.assertIn(channel_listen, msg.channel_ids)
+
+ # The user should have a new unread message
+ lead_user = lead.with_user(self.user_sales_manager)
+ self.assertTrue(lead_user.message_needaction)
+
+ def test_new_lead_from_email_multicompany(self):
+ company0 = self.env.company
+ company1 = self.env['res.company'].create({'name': 'new_company'})
+
+ self.env.user.write({
+ 'company_ids': [(4, company0.id, False), (4, company1.id, False)],
+ })
+
+ crm_team_model = self.env['ir.model'].search([('model', '=', 'crm.team')])
+ crm_lead_model = self.env['ir.model'].search([('model', '=', 'crm.lead')])
+ self.env["ir.config_parameter"].sudo().set_param("mail.catchall.domain", 'aqualung.com')
+
+ crm_team0 = self.env['crm.team'].create({
+ 'name': 'crm team 0',
+ 'company_id': company0.id,
+ })
+ crm_team1 = self.env['crm.team'].create({
+ 'name': 'crm team 1',
+ 'company_id': company1.id,
+ })
+
+ mail_alias0 = self.env['mail.alias'].create({
+ 'alias_name': 'sale_team_0',
+ 'alias_model_id': crm_lead_model.id,
+ 'alias_parent_model_id': crm_team_model.id,
+ 'alias_parent_thread_id': crm_team0.id,
+ 'alias_defaults': "{'type': 'opportunity', 'team_id': %s}" % crm_team0.id,
+ })
+ mail_alias1 = self.env['mail.alias'].create({
+ 'alias_name': 'sale_team_1',
+ 'alias_model_id': crm_lead_model.id,
+ 'alias_parent_model_id': crm_team_model.id,
+ 'alias_parent_thread_id': crm_team1.id,
+ 'alias_defaults': "{'type': 'opportunity', 'team_id': %s}" % crm_team1.id,
+ })
+
+ crm_team0.write({'alias_id': mail_alias0.id})
+ crm_team1.write({'alias_id': mail_alias1.id})
+
+ new_message0 = """MIME-Version: 1.0
+Date: Thu, 27 Dec 2018 16:27:45 +0100
+Message-ID: blablabla0
+Subject: sale team 0 in company 0
+From: A client <client_a@someprovider.com>
+To: sale_team_0@aqualung.com
+Content-Type: multipart/alternative; boundary="000000000000a47519057e029630"
+
+--000000000000a47519057e029630
+Content-Type: text/plain; charset="UTF-8"
+
+
+--000000000000a47519057e029630
+Content-Type: text/html; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+<div>A good message</div>
+
+--000000000000a47519057e029630--
+"""
+
+ new_message1 = """MIME-Version: 1.0
+Date: Thu, 27 Dec 2018 16:27:45 +0100
+Message-ID: blablabla1
+Subject: sale team 1 in company 1
+From: B client <client_b@someprovider.com>
+To: sale_team_1@aqualung.com
+Content-Type: multipart/alternative; boundary="000000000000a47519057e029630"
+
+--000000000000a47519057e029630
+Content-Type: text/plain; charset="UTF-8"
+
+
+--000000000000a47519057e029630
+Content-Type: text/html; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+<div>A good message bis</div>
+
+--000000000000a47519057e029630--
+"""
+ crm_lead0_id = self.env['mail.thread'].message_process('crm.lead', new_message0)
+ crm_lead1_id = self.env['mail.thread'].message_process('crm.lead', new_message1)
+
+ crm_lead0 = self.env['crm.lead'].browse(crm_lead0_id)
+ crm_lead1 = self.env['crm.lead'].browse(crm_lead1_id)
+
+ self.assertEqual(crm_lead0.team_id, crm_team0)
+ self.assertEqual(crm_lead1.team_id, crm_team1)
+
+ self.assertEqual(crm_lead0.company_id, company0)
+ self.assertEqual(crm_lead1.company_id, company1)
diff --git a/addons/crm/tests/test_crm_pls.py b/addons/crm/tests/test_crm_pls.py
new file mode 100644
index 00000000..2d6db0d1
--- /dev/null
+++ b/addons/crm/tests/test_crm_pls.py
@@ -0,0 +1,401 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import timedelta
+
+from odoo import fields, tools
+from odoo.tests.common import TransactionCase
+
+
+class TestCRMPLS(TransactionCase):
+
+ def _get_lead_values(self, team_id, name_suffix, country_id, state_id, email_state, phone_state, source_id, stage_id):
+ return {
+ 'name': 'lead_' + name_suffix,
+ 'type': 'opportunity',
+ 'state_id': state_id,
+ 'email_state': email_state,
+ 'phone_state': phone_state,
+ 'source_id': source_id,
+ 'stage_id': stage_id,
+ 'country_id': country_id,
+ 'team_id': team_id
+ }
+
+ def generate_leads_with_tags(self, tag_ids):
+ Lead = self.env['crm.lead']
+ team_id = self.env['crm.team'].create({
+ 'name': 'blup',
+ }).id
+
+ leads_to_create = []
+ for i in range(150):
+ if i < 50: # tag 1
+ leads_to_create.append({
+ 'name': 'lead_tag_%s' % str(i),
+ 'tag_ids': [(4, tag_ids[0])],
+ 'team_id': team_id
+ })
+ elif i < 100: # tag 2
+ leads_to_create.append({
+ 'name': 'lead_tag_%s' % str(i),
+ 'tag_ids': [(4, tag_ids[1])],
+ 'team_id': team_id
+ })
+ else: # tag 1 and 2
+ leads_to_create.append({
+ 'name': 'lead_tag_%s' % str(i),
+ 'tag_ids': [(6, 0, tag_ids)],
+ 'team_id': team_id
+ })
+
+ leads_with_tags = Lead.create(leads_to_create)
+
+ return leads_with_tags
+
+ def test_predictive_lead_scoring(self):
+ """ We test here computation of lead probability based on PLS Bayes.
+ We will use 3 different values for each possible variables:
+ country_id : 1,2,3
+ state_id: 1,2,3
+ email_state: correct, incorrect, None
+ phone_state: correct, incorrect, None
+ source_id: 1,2,3
+ stage_id: 1,2,3 + the won stage
+ And we will compute all of this for 2 different team_id
+ Note : We assume here that original bayes computation is correct
+ as we don't compute manually the probabilities."""
+ Lead = self.env['crm.lead']
+ LeadScoringFrequency = self.env['crm.lead.scoring.frequency']
+ state_values = ['correct', 'incorrect', None]
+ source_ids = self.env['utm.source'].search([], limit=3).ids
+ state_ids = self.env['res.country.state'].search([], limit=3).ids
+ country_ids = self.env['res.country'].search([], limit=3).ids
+ stage_ids = self.env['crm.stage'].search([], limit=3).ids
+ won_stage_id = self.env['crm.stage'].search([('is_won', '=', True)], limit=1).id
+ team_ids = self.env['crm.team'].create([{'name': 'Team Test 1'}, {'name': 'Team Test 2'}]).ids
+ # create bunch of lost and won crm_lead
+ leads_to_create = []
+ # for team 1
+ for i in range(3):
+ leads_to_create.append(
+ self._get_lead_values(team_ids[0], 'team_1_%s' % str(i), country_ids[i], state_ids[i], state_values[i], state_values[i], source_ids[i], stage_ids[i]))
+ leads_to_create.append(
+ self._get_lead_values(team_ids[0], 'team_1_%s' % str(3), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[2], stage_ids[1]))
+ leads_to_create.append(
+ self._get_lead_values(team_ids[0], 'team_1_%s' % str(4), country_ids[1], state_ids[1], state_values[1], state_values[0], source_ids[1], stage_ids[0]))
+ # for team 2
+ leads_to_create.append(
+ self._get_lead_values(team_ids[1], 'team_2_%s' % str(5), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[1], stage_ids[2]))
+ leads_to_create.append(
+ self._get_lead_values(team_ids[1], 'team_2_%s' % str(6), country_ids[0], state_ids[1], state_values[0], state_values[1], source_ids[2], stage_ids[1]))
+ leads_to_create.append(
+ self._get_lead_values(team_ids[1], 'team_2_%s' % str(7), country_ids[0], state_ids[2], state_values[0], state_values[1], source_ids[2], stage_ids[0]))
+ leads_to_create.append(
+ self._get_lead_values(team_ids[1], 'team_2_%s' % str(8), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[2], stage_ids[1]))
+ leads_to_create.append(
+ self._get_lead_values(team_ids[1], 'team_2_%s' % str(9), country_ids[1], state_ids[0], state_values[1], state_values[0], source_ids[1], stage_ids[1]))
+
+ leads = Lead.create(leads_to_create)
+
+ # Set the PLS config
+ self.env['ir.config_parameter'].sudo().set_param("crm.pls_start_date", "2000-01-01")
+ self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", "country_id,state_id,email_state,phone_state,source_id")
+
+ # set leads as won and lost
+ # for Team 1
+ leads[0].action_set_lost()
+ leads[1].action_set_lost()
+ leads[2].action_set_won()
+ # for Team 2
+ leads[5].action_set_lost()
+ leads[6].action_set_lost()
+ leads[7].action_set_won()
+
+ # A. Test Full Rebuild
+ # rebuild frequencies table and recompute automated_probability for all leads.
+ Lead._cron_update_automated_probabilities()
+
+ # As the cron is computing and writing in SQL queries, we need to invalidate the cache
+ leads.invalidate_cache()
+
+ self.assertEqual(tools.float_compare(leads[3].automated_probability, 33.49, 2), 0)
+ self.assertEqual(tools.float_compare(leads[8].automated_probability, 7.74, 2), 0)
+
+ # Test frequencies
+ lead_4_stage_0_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', stage_ids[0])])
+ lead_4_stage_won_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', won_stage_id)])
+ lead_4_country_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[4].country_id.id)])
+ lead_4_email_state_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'email_state'), ('value', '=', str(leads[4].email_state))])
+
+ lead_9_stage_0_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', stage_ids[0])])
+ lead_9_stage_won_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', won_stage_id)])
+ lead_9_country_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[9].country_id.id)])
+ lead_9_email_state_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'email_state'), ('value', '=', str(leads[9].email_state))])
+
+ self.assertEqual(lead_4_stage_0_freq.won_count, 1.1)
+ self.assertEqual(lead_4_stage_won_freq.won_count, 1.1)
+ self.assertEqual(lead_4_country_freq.won_count, 0.1)
+ self.assertEqual(lead_4_email_state_freq.won_count, 1.1)
+ self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1)
+ self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1)
+ self.assertEqual(lead_4_country_freq.lost_count, 1.1)
+ self.assertEqual(lead_4_email_state_freq.lost_count, 2.1)
+
+ self.assertEqual(lead_9_stage_0_freq.won_count, 1.1)
+ self.assertEqual(lead_9_stage_won_freq.won_count, 1.1)
+ self.assertEqual(lead_9_country_freq.won_count, 0.0) # frequency does not exist
+ self.assertEqual(lead_9_email_state_freq.won_count, 1.1)
+ self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1)
+ self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1)
+ self.assertEqual(lead_9_country_freq.lost_count, 0.0) # frequency does not exist
+ self.assertEqual(lead_9_email_state_freq.lost_count, 2.1)
+
+ # B. Test Live Increment
+ leads[4].action_set_lost()
+ leads[9].action_set_won()
+
+ # re-get frequencies that did not exists before
+ lead_9_country_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[9].country_id.id)])
+
+ # B.1. Test frequencies - team 1 should not impact team 2
+ self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
+ self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # + 1
+ self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) # unchanged - consider stages with <= sequence when lost
+ self.assertEqual(lead_4_country_freq.lost_count, 2.1) # + 1
+ self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # + 1
+
+ self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # + 1
+ self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # + 1 - consider every stages when won
+ self.assertEqual(lead_9_country_freq.won_count, 1.1) # + 1
+ self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # + 1
+ self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged
+ self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged
+ self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged (did not exists before)
+ self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged
+
+ # Propabilities of other leads should not be impacted as only modified lead are recomputed.
+ self.assertEqual(tools.float_compare(leads[3].automated_probability, 33.49, 2), 0)
+ self.assertEqual(tools.float_compare(leads[8].automated_probability, 7.74, 2), 0)
+
+ self.assertEqual(leads[3].is_automated_probability, True)
+ self.assertEqual(leads[8].is_automated_probability, True)
+
+ # Restore -> Should decrease lost
+ leads[4].toggle_active()
+ self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
+ self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # - 1
+ self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) # unchanged - consider stages with <= sequence when lost
+ self.assertEqual(lead_4_country_freq.lost_count, 1.1) # - 1
+ self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # - 1
+
+ self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # unchanged
+ self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # unchanged
+ self.assertEqual(lead_9_country_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # unchanged
+ self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged
+ self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged
+ self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged
+ self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged
+
+ # set to won stage -> Should increase won
+ leads[4].stage_id = won_stage_id
+ self.assertEqual(lead_4_stage_0_freq.won_count, 2.1) # + 1
+ self.assertEqual(lead_4_stage_won_freq.won_count, 2.1) # + 1
+ self.assertEqual(lead_4_country_freq.won_count, 1.1) # + 1
+ self.assertEqual(lead_4_email_state_freq.won_count, 2.1) # + 1
+ self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # unchanged
+ self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) # unchanged
+ self.assertEqual(lead_4_country_freq.lost_count, 1.1) # unchanged
+ self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # unchanged
+
+ # Archive (was won, now lost) -> Should decrease won and increase lost
+ leads[4].toggle_active()
+ self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # - 1
+ self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # - 1
+ self.assertEqual(lead_4_country_freq.won_count, 0.1) # - 1
+ self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # - 1
+ self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # + 1
+ self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # consider stages with <= sequence when lostand as stage is won.. even won_stage lost_count is increased by 1
+ self.assertEqual(lead_4_country_freq.lost_count, 2.1) # + 1
+ self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # + 1
+
+ # Move to original stage -> Should do nothing (as lead is still lost)
+ leads[4].stage_id = stage_ids[0]
+ self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
+ self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # unchanged
+ self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # unchanged
+ self.assertEqual(lead_4_country_freq.lost_count, 2.1) # unchanged
+ self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # unchanged
+
+ # Restore -> Should decrease lost - at the end, frequencies should be like first frequencyes tests (except for 0.0 -> 0.1)
+ leads[4].toggle_active()
+ self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
+ self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # - 1
+ self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # unchanged - consider stages with <= sequence when lost
+ self.assertEqual(lead_4_country_freq.lost_count, 1.1) # - 1
+ self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # - 1
+
+ # Probabilities should only be recomputed after modifying the lead itself.
+ leads[3].stage_id = stage_ids[0] # probability should only change a bit as frequencies are almost the same (except 0.0 -> 0.1)
+ leads[8].stage_id = stage_ids[0] # probability should change quite a lot
+
+ # Test frequencies (should not have changed)
+ self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
+ self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # unchanged
+ self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # unchanged
+ self.assertEqual(lead_4_country_freq.lost_count, 1.1) # unchanged
+ self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # unchanged
+
+ self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # unchanged
+ self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # unchanged
+ self.assertEqual(lead_9_country_freq.won_count, 1.1) # unchanged
+ self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # unchanged
+ self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged
+ self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged
+ self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged
+ self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged
+
+ # Continue to test probability computation
+ leads[3].probability = 40
+
+ self.assertEqual(leads[3].is_automated_probability, False)
+ self.assertEqual(leads[8].is_automated_probability, True)
+
+ self.assertEqual(tools.float_compare(leads[3].automated_probability, 20.87, 2), 0)
+ self.assertEqual(tools.float_compare(leads[8].automated_probability, 2.43, 2), 0)
+ self.assertEqual(tools.float_compare(leads[3].probability, 40, 2), 0)
+ self.assertEqual(tools.float_compare(leads[8].probability, 2.43, 2), 0)
+
+ # Test modify country_id
+ leads[8].country_id = country_ids[1]
+ self.assertEqual(tools.float_compare(leads[8].automated_probability, 34.38, 2), 0)
+ self.assertEqual(tools.float_compare(leads[8].probability, 34.38, 2), 0)
+
+ leads[8].country_id = country_ids[0]
+ self.assertEqual(tools.float_compare(leads[8].automated_probability, 2.43, 2), 0)
+ self.assertEqual(tools.float_compare(leads[8].probability, 2.43, 2), 0)
+
+ # ----------------------------------------------
+ # Test tag_id frequencies and probability impact
+ # ----------------------------------------------
+
+ tag_ids = self.env['crm.tag'].create([
+ {'name': "Tag_test_1"},
+ {'name': "Tag_test_2"},
+ ]).ids
+ # tag_ids = self.env['crm.tag'].search([], limit=2).ids
+ leads_with_tags = self.generate_leads_with_tags(tag_ids)
+
+ leads_with_tags[:30].action_set_lost() # 60% lost on tag 1
+ leads_with_tags[31:50].action_set_won() # 40% won on tag 1
+ leads_with_tags[50:90].action_set_lost() # 80% lost on tag 2
+ leads_with_tags[91:100].action_set_won() # 20% won on tag 2
+ leads_with_tags[100:135].action_set_lost() # 70% lost on tag 1 and 2
+ leads_with_tags[136:150].action_set_won() # 30% won on tag 1 and 2
+ # tag 1 : won = 19+14 / lost = 30+35
+ # tag 2 : won = 9+14 / lost = 40+35
+
+ tag_1_freq = LeadScoringFrequency.search([('variable', '=', 'tag_id'), ('value', '=', tag_ids[0])])
+ tag_2_freq = LeadScoringFrequency.search([('variable', '=', 'tag_id'), ('value', '=', tag_ids[1])])
+ self.assertEqual(tools.float_compare(tag_1_freq.won_count, 33.1, 1), 0)
+ self.assertEqual(tools.float_compare(tag_1_freq.lost_count, 65.1, 1), 0)
+ self.assertEqual(tools.float_compare(tag_2_freq.won_count, 23.1, 1), 0)
+ self.assertEqual(tools.float_compare(tag_2_freq.lost_count, 75.1, 1), 0)
+
+ # Force recompute - A priori, no need to do this as, for each won / lost, we increment tag frequency.
+ Lead._cron_update_automated_probabilities()
+ leads_with_tags.invalidate_cache()
+
+ lead_tag_1 = leads_with_tags[30]
+ lead_tag_2 = leads_with_tags[90]
+ lead_tag_1_2 = leads_with_tags[135]
+
+ self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 33.69, 2), 0)
+ self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0)
+ self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 28.05, 2), 0)
+
+ lead_tag_1.tag_ids = [(5, 0, 0)] # remove all tags
+ lead_tag_1_2.tag_ids = [(3, tag_ids[1], 0)] # remove tag 2
+
+ self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0)
+ self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0) # no impact
+ self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 33.69, 2), 0)
+
+ lead_tag_1.tag_ids = [(4, tag_ids[1])] # add tag 2
+ lead_tag_2.tag_ids = [(4, tag_ids[0])] # add tag 1
+ lead_tag_1_2.tag_ids = [(3, tag_ids[0]), (4, tag_ids[1])] # remove tag 1 / add tag 2
+
+ self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 23.51, 2), 0)
+ self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 28.05, 2), 0)
+ self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 23.51, 2), 0)
+
+ # go back to initial situation
+ lead_tag_1.tag_ids = [(3, tag_ids[1]), (4, tag_ids[0])] # remove tag 2 / add tag 1
+ lead_tag_2.tag_ids = [(3, tag_ids[0])] # remove tag 1
+ lead_tag_1_2.tag_ids = [(4, tag_ids[0])] # add tag 1
+
+ self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 33.69, 2), 0)
+ self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0)
+ self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 28.05, 2), 0)
+
+ # set email_state for each lead and update probabilities
+ leads.filtered(lambda lead: lead.id % 2 == 0).email_state = 'correct'
+ leads.filtered(lambda lead: lead.id % 2 == 1).email_state = 'incorrect'
+ Lead._cron_update_automated_probabilities()
+ leads_with_tags.invalidate_cache()
+
+ self.assertEqual(tools.float_compare(leads[3].automated_probability, 4.21, 2), 0)
+ self.assertEqual(tools.float_compare(leads[8].automated_probability, 0.23, 2), 0)
+
+ # remove all pls fields
+ self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", False)
+ Lead._cron_update_automated_probabilities()
+ leads_with_tags.invalidate_cache()
+
+ self.assertEqual(tools.float_compare(leads[3].automated_probability, 34.38, 2), 0)
+ self.assertEqual(tools.float_compare(leads[8].automated_probability, 50.0, 2), 0)
+
+ # check if the probabilities are the same with the old param
+ self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", "country_id,state_id,email_state,phone_state,source_id")
+ Lead._cron_update_automated_probabilities()
+ leads_with_tags.invalidate_cache()
+
+ self.assertEqual(tools.float_compare(leads[3].automated_probability, 4.21, 2), 0)
+ self.assertEqual(tools.float_compare(leads[8].automated_probability, 0.23, 2), 0)
+
+ def test_settings_pls_start_date(self):
+ # We test here that settings never crash due to ill-configured config param 'crm.pls_start_date'
+ set_param = self.env['ir.config_parameter'].sudo().set_param
+ str_date_8_days_ago = fields.Date.to_string(fields.Date.today() - timedelta(days=8))
+ resConfig = self.env['res.config.settings']
+
+ set_param("crm.pls_start_date", "2021-10-10")
+ res_config_new = resConfig.new()
+ self.assertEqual(fields.Date.to_string(res_config_new.predictive_lead_scoring_start_date),
+ "2021-10-10", "If config param is a valid date, date in settings should match with config param")
+
+ set_param("crm.pls_start_date", "")
+ res_config_new = resConfig.new()
+ self.assertEqual(fields.Date.to_string(res_config_new.predictive_lead_scoring_start_date),
+ str_date_8_days_ago, "If config param is empty, date in settings should be set to 8 days before today")
+
+ set_param("crm.pls_start_date", "One does not simply walk into system parameters to corrupt them")
+ res_config_new = resConfig.new()
+ self.assertEqual(fields.Date.to_string(res_config_new.predictive_lead_scoring_start_date),
+ str_date_8_days_ago, "If config param is not a valid date, date in settings should be set to 8 days before today")
diff --git a/addons/crm/tests/test_crm_ui.py b/addons/crm/tests/test_crm_ui.py
new file mode 100644
index 00000000..116163f2
--- /dev/null
+++ b/addons/crm/tests/test_crm_ui.py
@@ -0,0 +1,24 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import odoo.tests
+
+
+@odoo.tests.tagged('post_install','-at_install')
+class TestUi(odoo.tests.HttpCase):
+
+ def test_01_crm_tour(self):
+ self.start_tour("/web", 'crm_tour', login="admin")
+
+ def test_02_crm_tour_rainbowman(self):
+ # we create a new user to make sure he gets the 'Congrats on your first deal!'
+ # rainbowman message.
+ self.env['res.users'].create({
+ 'name': 'Temporary CRM User',
+ 'login': 'temp_crm_user',
+ 'password': 'temp_crm_user',
+ 'groups_id': [(6, 0, [
+ self.ref('base.group_user'),
+ self.ref('sales_team.group_sale_salesman')
+ ])]
+ })
+ self.start_tour("/web", 'crm_rainbowman', login="temp_crm_user")