diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/crm/tests | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/crm/tests')
| -rw-r--r-- | addons/crm/tests/__init__.py | 11 | ||||
| -rw-r--r-- | addons/crm/tests/common.py | 385 | ||||
| -rw-r--r-- | addons/crm/tests/test_crm_activity.py | 177 | ||||
| -rw-r--r-- | addons/crm/tests/test_crm_lead.py | 487 | ||||
| -rw-r--r-- | addons/crm/tests/test_crm_lead_convert.py | 484 | ||||
| -rw-r--r-- | addons/crm/tests/test_crm_lead_convert_mass.py | 212 | ||||
| -rw-r--r-- | addons/crm/tests/test_crm_lead_lost.py | 65 | ||||
| -rw-r--r-- | addons/crm/tests/test_crm_lead_merge.py | 143 | ||||
| -rw-r--r-- | addons/crm/tests/test_crm_lead_notification.py | 135 | ||||
| -rw-r--r-- | addons/crm/tests/test_crm_pls.py | 401 | ||||
| -rw-r--r-- | addons/crm/tests/test_crm_ui.py | 24 |
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") |
