19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:53 +01:00
parent dc68f80d3f
commit 7221b9ac46
610 changed files with 135477 additions and 161677 deletions

View file

@ -8,12 +8,13 @@ from . import test_crm_lead_notification
from . import test_crm_lead_convert
from . import test_crm_lead_convert_mass
from . import test_crm_lead_duplicates
from . import test_crm_lead_lost
from . import test_crm_lead_merge
from . import test_crm_lead_multicompany
from . import test_crm_lead_smart_calendar
from . import test_crm_ui
from . import test_crm_pls
from . import test_crm_rainbowman
from . import test_digest
from . import test_performances
from . import test_res_partner
from . import test_sales_team_ui

View file

@ -2,7 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from collections import defaultdict
from contextlib import contextmanager
from datetime import timedelta
from unittest.mock import patch
from odoo.addons.crm.models.crm_lead import PARTNER_ADDRESS_FIELDS_TO_SYNC
@ -37,7 +39,7 @@ Content-Transfer-Encoding: 8bit
This is an example email. All sensitive content has been stripped out.
ALL GLORY TO THE HYPNOTOAD !
ALL GLORY TO THE HYPNOTOAD!
Cheers,
@ -49,9 +51,9 @@ class TestCrmCommon(TestSalesCommon, MailCase):
FIELDS_FIRST_SET = [
'name', 'partner_id', 'campaign_id', 'company_id', 'country_id',
'team_id', 'state_id', 'stage_id', 'medium_id', 'source_id', 'user_id',
'title', 'city', 'contact_name', 'mobile', 'partner_name',
'city', 'contact_name', 'partner_name',
'phone', 'probability', 'expected_revenue', 'street', 'street2', 'zip',
'create_date', 'date_action_last', 'email_from', 'email_cc', 'website'
'create_date', 'date_automation_last', 'email_from', 'email_cc', 'website'
]
merge_fields = ['description', 'type', 'priority']
@ -91,35 +93,35 @@ class TestCrmCommon(TestSalesCommon, MailCase):
})
(cls.user_sales_manager + cls.user_sales_leads + cls.user_sales_salesman).write({
'groups_id': [(4, cls.env.ref('crm.group_use_lead').id)]
'group_ids': [(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,
'team_ids': [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,
'team_ids': [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,
'team_ids': [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,
'team_ids': False,
})
cls.stage_gen_won = cls.env['crm.stage'].create({
'name': 'Generic Won',
'sequence': 30,
'team_id': False,
'team_ids': False,
'is_won': True,
})
@ -158,7 +160,7 @@ class TestCrmCommon(TestSalesCommon, MailCase):
})
cls.lead_team_1_won.action_set_won()
cls.lead_team_1_lost = cls.env['crm.lead'].create({
'name': 'Already Won',
'name': 'Already Lost',
'type': 'lead',
'user_id': cls.user_sales_leads.id,
'team_id': cls.sales_team_1.id,
@ -166,6 +168,11 @@ class TestCrmCommon(TestSalesCommon, MailCase):
cls.lead_team_1_lost.action_set_lost()
(cls.lead_team_1_won + cls.lead_team_1_lost).flush_recordset()
# make lead 1 take team history into account for its automated proba.
# it should now be 50% as auto proba. (1 lost 1 won for team 1)
cls.lead_1._compute_probabilities()
cls.lead_1.flush_recordset()
# email / phone data
cls.test_email_data = [
'"Planet Express" <planet.express@test.example.com>',
@ -201,8 +208,6 @@ class TestCrmCommon(TestSalesCommon, MailCase):
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',
'lang': cls.lang_en.code,
'phone': False,
@ -217,7 +222,6 @@ class TestCrmCommon(TestSalesCommon, MailCase):
'name': 'Turanga Leela',
'email': cls.test_email_data[2],
'lang': cls.lang_en.code,
'mobile': cls.test_phone_data[1],
'phone': cls.test_phone_data[2],
'parent_id': False,
'is_company': False,
@ -234,7 +238,6 @@ class TestCrmCommon(TestSalesCommon, MailCase):
'city': 'New new York',
'country_id': base_us.id,
'lang': cls.lang_en.code,
'mobile': '+1 202 555 0888',
'zip': '87654',
})
@ -253,10 +256,6 @@ class TestCrmCommon(TestSalesCommon, MailCase):
'res_id': cls.activity_type_1.id,
})
def setUp(self):
super(TestCrmCommon, self).setUp()
self.flush_tracking()
@classmethod
def _activate_multi_company(cls):
cls.company_2 = cls.env['res.company'].create({
@ -265,6 +264,18 @@ class TestCrmCommon(TestSalesCommon, MailCase):
'email': 'company.2@test.example.com',
'name': 'New Test Company',
})
cls.alias_bounce_c2 = 'bounce.c2'
cls.alias_catchall_c2 = 'catchall.c2'
cls.alias_default_from_c2 = 'notifications.c2'
cls.alias_domain_c2_name = 'test.mycompany2.com'
cls.mail_alias_domain_c2 = cls.env['mail.alias.domain'].create({
'bounce_alias': cls.alias_bounce_c2,
'catchall_alias': cls.alias_catchall_c2,
'company_ids': [(4, cls.company_2.id)],
'default_from': cls.alias_default_from_c2,
'name': cls.alias_domain_c2_name,
'sequence': 2,
})
cls.user_sales_manager_mc = mail_new_test_user(
cls.env,
@ -305,7 +316,7 @@ class TestCrmCommon(TestSalesCommon, MailCase):
def _create_leads_batch(self, lead_type='lead', count=10, email_dup_count=0,
partner_count=0, partner_ids=None, user_ids=None,
country_ids=None, probabilities=None, suffix=''):
country_ids=None, probabilities=None, suffix='', additional_lead_values=defaultdict(None)):
""" Helper tool method creating a batch of leads, useful when dealing
with batch processes. Please update me.
@ -323,6 +334,7 @@ class TestCrmCommon(TestSalesCommon, MailCase):
'name': f'TestLead{suffix}_{x:04d}',
'type': lead_type if lead_type else types[x % 2],
'priority': '%s' % (x % 3),
**additional_lead_values,
} for x in range(count)]
# generate customer information
@ -381,7 +393,7 @@ class TestCrmCommon(TestSalesCommon, MailCase):
# duplicates (currently only with email)
dups_data = []
if email_dup_count and not partner_ids:
for idx, lead_data in enumerate(leads_data):
for lead_data in leads_data:
if not lead_data.get('partner_id') and lead_data['email_from']:
dup_data = dict(lead_data)
dup_data['name'] = 'Duplicated-%s' % dup_data['name']
@ -456,6 +468,8 @@ class TestCrmCommon(TestSalesCommon, MailCase):
:param leads: merged leads (including opportunity)
"""
self.assertIn(opportunity, leads)
opportunity = opportunity.sudo()
leads = leads.sudo()
# save opportunity value before being modified by merge process
fields_all = self.FIELDS_FIRST_SET + self.merge_fields
@ -498,11 +512,11 @@ class TestCrmCommon(TestSalesCommon, MailCase):
yield
finally:
# support specific values caller may want to check in addition to generic tests
for fname, expected in expected.items():
if expected is False:
for fname, e_val in expected.items():
if e_val is False:
self.assertFalse(opportunity[fname], "%s must be False" % fname)
else:
self.assertEqual(opportunity[fname], expected, "%s must be equal to %s" % (fname, expected))
self.assertEqual(opportunity[fname], e_val, "%s must be equal to %s" % (fname, e_val))
# classic fields: first not void wins or specific computation
for fname in fields_all:
@ -568,7 +582,7 @@ class TestLeadConvertCommon(TestCrmCommon):
cls.stage_team_convert_1 = cls.env['crm.stage'].create({
'name': 'New',
'sequence': 1,
'team_id': cls.sales_team_convert.id,
'team_ids': [cls.sales_team_convert.id],
})
cls.lead_1.write({'date_open': Datetime.from_string('2020-01-15 11:30:00')})
@ -632,8 +646,12 @@ class TestLeadConvertCommon(TestCrmCommon):
def assertMemberAssign(self, member, count):
""" Check assign result and that domains are effectively taken into account """
self.assertEqual(member.lead_month_count, count)
member_leads = self.env['crm.lead'].search(member._get_lead_month_domain())
self.assertEqual(member.lead_day_count, count)
member_leads = self.env['crm.lead'].search([
('user_id', '=', member.user_id.id),
('team_id', '=', member.crm_team_id.id),
('date_open', '>=', Datetime.now() - timedelta(hours=24)),
])
self.assertEqual(len(member_leads), count)
if member.assignment_domain:
self.assertEqual(

View file

@ -4,14 +4,15 @@
from datetime import date, timedelta
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.tests.common import users
from odoo.tests.common import tagged, users
@tagged('mail_activity')
class TestCrmMailActivity(TestCrmCommon):
@classmethod
def setUpClass(cls):
super(TestCrmMailActivity, cls).setUpClass()
super().setUpClass()
cls.activity_type_1 = cls.env['mail.activity.type'].create({
'name': 'Initial Contact',
@ -22,7 +23,7 @@ class TestCrmMailActivity(TestCrmCommon):
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 !',
'summary': 'ACT 2 : I want to show you my ERP!',
'res_model': 'crm.lead',
})
for activity_type in cls.activity_type_1 + cls.activity_type_2:
@ -70,10 +71,10 @@ class TestCrmMailActivity(TestCrmCommon):
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[0:3].activity_schedule(act_type_xmlid='crm.initial_contact', user_id=self.user_sales_leads.id, date_deadline=deadline_in2d)
test_leads[5].activity_schedule(act_type_xmlid='crm.initial_contact', user_id=self.user_sales_leads.id, date_deadline=deadline_in2d)
(test_leads[1] | test_leads[3]).activity_schedule(act_type_xmlid='crm.initial_contact', user_id=self.user_sales_leads.id, date_deadline=deadline_was1d)
(test_leads[2] | test_leads[4]).activity_schedule(act_type_xmlid='crm.call_for_demo', user_id=self.user_sales_leads.id, date_deadline=deadline_was2d)
test_leads.invalidate_recordset()
expected_ids_asc = [2, 4, 1, 3, 5, 0, 8, 7, 9, 6]
@ -137,8 +138,9 @@ class TestCrmMailActivity(TestCrmCommon):
# 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)
self.lead_1.invalidate_recordset(fnames=["activity_type_id"]) # archive does not trigger recompute
self.assertFalse(self.lead_1.activity_type_id)
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'))
@ -146,18 +148,19 @@ class TestCrmMailActivity(TestCrmCommon):
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)
test_lead = self.lead_1.with_user(self.user_sales_manager)
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_id': test_lead.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(test_lead.activity_summary, activity.summary)
self.assertEqual(test_lead.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({
@ -167,11 +170,14 @@ class TestCrmMailActivity(TestCrmCommon):
})
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(test_lead.activity_summary, activity.activity_type_id.summary)
self.assertEqual(test_lead.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))
self.assertEqual(test_lead.activity_ids, activity)
activity.action_done()
# Check the next activity on the lead has been removed
self.assertFalse(self.lead_1.activity_type_id)
self.assertFalse(test_lead.activity_ids)
test_lead.invalidate_recordset(fnames=["activity_type_id"]) # archive does not trigger recompute
self.assertFalse(test_lead.activity_type_id)

View file

@ -5,12 +5,14 @@ from datetime import datetime
from freezegun import freeze_time
from unittest.mock import patch
from odoo import fields
from odoo.addons.base.tests.test_format_address_mixin import FormatAddressCase
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.mail.tests.common_tracking import MailTrackingDurationMixinCase
from odoo.addons.phone_validation.tools.phone_validation import phone_format
from odoo.exceptions import UserError
from odoo.tests.common import Form, tagged, users
from odoo.tests import Form, tagged, users
from odoo.tools import mute_logger
@ -76,6 +78,37 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(self.contact_company_1.lang, self.lang_en.code)
self.assertEqual(lead.lang_id, self.lang_en)
@users('user_sales_leads')
def test_crm_lead_compute_commercial_partner(self):
company_partner, child_partner, orphan_partner = self.env['res.partner'].create([
{
'name': 'test_crm_lead_compute_commercial_partner',
'is_company': True,
'email': 'test_crm_lead_compute_commercial_partner@test.lan',
},
{'name': 'Test Child'},
{'name': 'Test Orphan'},
])
child_partner.parent_id = company_partner
lead = self.env['crm.lead'].create({
'name': 'Test Lead',
'partner_name': 'test_crm_lead_compute_commercial_partner',
})
self.assertEqual(lead.commercial_partner_id, company_partner)
lead.partner_id = orphan_partner
self.assertFalse(lead.commercial_partner_id)
lead.partner_id = child_partner
self.assertEqual(lead.commercial_partner_id, company_partner)
lead.write({
'partner_id': False,
'partner_name': False,
})
self.assertFalse(lead.commercial_partner_id)
lead.partner_id = company_partner
# this is mostly because we use it to set "parent_id" in most flows
# and it doesn't really make sense to have it be its own parent
self.assertFalse(lead.commercial_partner_id, "If a partner is its own commercial_partner_id, the lead is considered to have none.")
@users('user_sales_leads')
def test_crm_lead_creation_no_partner(self):
lead_data = {
@ -184,8 +217,7 @@ class TestCRMLead(TestCrmCommon):
'name': 'Empty partner',
'is_company': True,
'lang': 'en_US',
'mobile': '123456789',
'title': self.env.ref('base.res_partner_title_mister').id,
'phone': '0485112233',
'function': 'My function',
})
lead_data = {
@ -195,7 +227,6 @@ class TestCRMLead(TestCrmCommon):
'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)
@ -214,8 +245,6 @@ class TestCRMLead(TestCrmCommon):
# PARTNER_FIELDS_TO_SYNC
self.assertEqual(lead.lang_id, self.lang_en)
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")
@ -247,27 +276,43 @@ class TestCRMLead(TestCrmCommon):
@users('user_sales_manager')
def test_crm_lead_currency_sync(self):
lead = self.env['crm.lead'].create({
lead_company = self.env['res.company'].sudo().create({
'name': 'EUR company',
'currency_id': self.env.ref('base.EUR').id,
})
lead = self.env['crm.lead'].with_company(lead_company).create({
'name': 'Lead 1',
'company_id': self.company_main.id
'company_id': lead_company.id
})
self.assertEqual(lead.company_currency, self.env.ref('base.EUR'))
self.company_main.currency_id = self.env.ref('base.CHF')
lead.with_company(self.company_main).update({'company_id': False})
lead_company.currency_id = self.env.ref('base.CHF')
lead.update({'company_id': False})
self.assertEqual(lead.company_currency, self.env.ref('base.CHF'))
#set back original currency
self.company_main.currency_id = self.env.ref('base.EUR')
@users('user_sales_manager')
def test_crm_lead_date_closed(self):
# ensure a lead created directly in a won stage gets a date_closed
lead_in_won = self.env['crm.lead'].create({
'name': 'Created in Won',
'type': 'opportunity',
'stage_id': self.stage_team1_won.id,
'expected_revenue': 123.45,
})
# date_closed must be set at creation when stage is won
self.assertTrue(lead_in_won.date_closed, "Lead created in a won stage must have date_closed set")
self.assertIsInstance(lead_in_won.date_closed, datetime)
# Test for one won lead
stage_team1_won2 = self.env['crm.stage'].create({
'name': 'Won2',
'sequence': 75,
'team_id': self.sales_team_1.id,
'team_ids': [self.sales_team_1.id],
'is_won': True,
})
old_date_closed = lead_in_won.date_closed
with freeze_time('2020-02-02 18:00'):
lead_in_won.stage_id = stage_team1_won2
self.assertEqual(lead_in_won.date_closed, old_date_closed, 'Moving between won stages should not change existing date_closed')
won_lead = self.lead_team_1_won.with_env(self.env)
other_lead = self.lead_1.with_env(self.env)
old_date_closed = won_lead.date_closed
@ -301,6 +346,36 @@ class TestCRMLead(TestCrmCommon):
lead.action_set_lost()
self.assertEqual(lead.date_closed, datetime.now(), "Closed date is updated after marking lead as lost")
@users('user_sales_manager')
def test_crm_lead_meeting_display_fields(self):
lead = self.env['crm.lead'].create({'name': 'Lead With Meetings'})
meeting_1, meeting_2, meeting_3 = self.env['calendar.event'].create([{
'name': 'Meeting 1 of Lead',
'opportunity_id': lead.id,
'start': '2022-07-12 08:00:00',
'stop': '2022-07-12 10:00:00',
}, {
'name': 'Meeting 2 of Lead',
'opportunity_id': lead.id,
'start': '2022-07-14 08:00:00',
'stop': '2022-07-14 10:00:00',
}, {
'name': 'Meeting 3 of Lead',
'opportunity_id': lead.id,
'start': '2022-07-15 08:00:00',
'stop': '2022-07-15 10:00:00',
}])
with freeze_time('2022-07-13 11:00:00'):
self.assertEqual(lead.meeting_display_date, fields.Date.from_string('2022-07-14'))
self.assertEqual(lead.meeting_display_label, 'Next Meeting')
(meeting_2 | meeting_3).unlink()
self.assertEqual(lead.meeting_display_date, fields.Date.from_string('2022-07-12'))
self.assertEqual(lead.meeting_display_label, 'Last Meeting')
meeting_1.unlink()
self.assertFalse(lead.meeting_display_date)
self.assertEqual(lead.meeting_display_label, 'No Meeting')
@users('user_sales_manager')
def test_crm_lead_partner_sync(self):
lead, partner = self.lead_1.with_user(self.env.user), self.contact_2
@ -325,11 +400,13 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(lead.email_from, partner_email)
self.assertEqual(lead.phone, '+1 202 555 6666')
# resetting lead values also resets partner
# resetting lead values should not reset partner: voiding lead info (because
# of some reasons) should not prevent from using the contact in other records
lead.email_from, lead.phone = False, False
self.assertFalse(partner.email)
self.assertFalse(partner.email_normalized)
self.assertFalse(partner.phone)
self.assertFalse(lead.email_from)
self.assertFalse(lead.phone)
self.assertEqual(partner.email, partner_email)
self.assertEqual(partner.phone, '+1 202 555 6666')
@users('user_sales_manager')
def test_crm_lead_partner_sync_email_phone(self):
@ -345,19 +422,14 @@ class TestCRMLead(TestCrmCommon):
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 = self.test_phone_data[2]
partner_phone_formatted = phone_format(partner_phone, 'US', '1', force_format='INTERNATIONAL')
partner_phone_sanitized = phone_format(partner_phone, 'US', '1', force_format='E164')
partner_mobile_formatted = phone_format(partner_mobile, 'US', '1', force_format='INTERNATIONAL')
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
@ -366,8 +438,6 @@ class TestCRMLead(TestCrmCommon):
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.partner_email_update)
self.assertFalse(lead_form.partner_phone_update)
@ -380,9 +450,7 @@ class TestCRMLead(TestCrmCommon):
'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,
self.assertEqual(lead.phone_sanitized, partner_phone_sanitized,
'Lead: phone_sanitized computed field on mobile')
# for email_from, if only formatting differs, warning should not appear and
@ -406,6 +474,7 @@ class TestCRMLead(TestCrmCommon):
self.assertTrue(lead_form.partner_email_update)
new_phone = '+1 202 555 7799'
new_phone_formatted = phone_format(new_phone, 'US', '1', force_format="INTERNATIONAL")
new_phone_sanitized = phone_format(new_phone, 'US', '1', force_format="E164")
lead_form.phone = new_phone
self.assertEqual(lead_form.phone, new_phone_formatted)
self.assertTrue(lead_form.partner_email_update)
@ -416,30 +485,21 @@ class TestCRMLead(TestCrmCommon):
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', force_format="INTERNATIONAL")
lead_form.mobile = new_mobile
# LEAD/PARTNER SYNC: resetting lead values should not reset partner
# # voiding lead info (because of some reasons) should not prevent
# # from using the contact in other records
lead_form.email_from, lead_form.phone = False, False
self.assertFalse(lead_form.partner_email_update)
self.assertFalse(lead_form.partner_phone_update)
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.assertTrue(lead_form.partner_email_update)
self.assertTrue(lead_form.partner_phone_update)
lead_form.save()
self.assertFalse(partner.email)
self.assertFalse(partner.email_normalized)
self.assertFalse(partner.phone)
self.assertEqual(partner.email, new_email)
self.assertEqual(partner.email_normalized, new_email_normalized)
self.assertEqual(partner.phone, new_phone_formatted)
self.assertFalse(lead.phone)
self.assertFalse(lead.mobile)
self.assertFalse(lead.phone_sanitized)
self.assertEqual(partner.mobile, partner_mobile)
# if SMS is uninstalled, phone_sanitized is not available on partner
if 'phone_sanitized' in partner:
self.assertEqual(partner.phone_sanitized, partner_mobile_sanitized,
self.assertEqual(partner.phone_sanitized, new_phone_sanitized,
'Partner sanitized should be computed on mobile')
@users('user_sales_manager')
@ -453,7 +513,6 @@ class TestCRMLead(TestCrmCommon):
'name': 'NoContact Partner',
'phone': '',
'email': '',
'mobile': '',
})
# This is a type == 'lead', not a type == 'opportunity'
@ -550,6 +609,41 @@ class TestCRMLead(TestCrmCommon):
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
def test_crm_lead_stages_with_multiple_possible_teams(self):
""" Test lead stage is properly set when switching between multiple teams. """
self.sales_team_2 = self.env['crm.team'].create({
'name': 'Test Sales Team 2',
'company_id': False,
'user_id': self.user_sales_manager.id,
})
self.sales_team_2_m1 = self.env['crm.team.member'].create({
'user_id': self.user_sales_leads.id,
'crm_team_id': self.sales_team_2.id,
})
user_teams = self.env['crm.team'].search([
('crm_team_member_all_ids.user_id', '=', self.user_sales_leads.id),
])
self.assertIn(self.sales_team_1, user_teams)
self.assertIn(self.sales_team_2, user_teams)
self.stage_team2_1 = self.env['crm.stage'].create({
'name': 'New (T2)',
'team_ids': [self.sales_team_2.id],
})
lead = self.env['crm.lead'].with_user(self.user_sales_leads).create({
'name': 'Test',
'contact_name': 'Test Contact',
'team_id': self.sales_team_1.id,
})
self.assertEqual(lead.team_id, self.sales_team_1)
self.assertEqual(lead.stage_id, self.stage_team1_1)
lead.team_id = self.sales_team_2
self.assertEqual(lead.team_id, self.sales_team_2)
self.assertEqual(lead.stage_id, self.stage_team2_1)
@users('user_sales_manager')
def test_crm_lead_unlink_calendar_event(self):
""" Test res_id / res_model is reset (and hide document button in calendar
@ -571,7 +665,7 @@ class TestCRMLead(TestCrmCommon):
'stop': '2022-07-13 10:00:00',
}
])
self.assertEqual(lead.calendar_event_count, 1)
self.assertEqual(len(lead.calendar_event_ids), 1)
self.assertEqual(meetings.opportunity_id, lead)
self.assertEqual(meetings.mapped('res_id'), [lead.id, lead.id])
self.assertEqual(meetings.mapped('res_model'), ['crm.lead', 'crm.lead'])
@ -728,28 +822,37 @@ class TestCRMLead(TestCrmCommon):
"""Test that the help message is the right one if we are on multiple team with different settings."""
# archive other teams
self.env['crm.team'].search([]).active = False
self.env['ir.config_parameter'].sudo().set_param("sales_team.membership_multi", True)
self._activate_multi_company()
team_other_comp = self.team_company2
user_team_leads, team_leads, user_team_opport, team_opport = self.env['crm.team'].create([{
'name': 'UserTeamLeads',
'company_id': self.env.company.id,
'use_leads': True,
'member_ids': [(6, 0, [self.env.user.id])],
}, {
'name': 'TeamLeads',
'company_id': self.env.company.id,
'use_leads': True,
'member_ids': [],
}, {
'name': 'UserTeamOpportunities',
'company_id': self.env.company.id,
'use_leads': False,
'member_ids': [(6, 0, [self.env.user.id])],
}, {
'name': 'TeamOpportunities',
'company_id': self.env.company.id,
'use_leads': False,
'member_ids': [],
}])
# Additional check to ensure proper team creation
user_team_leads.invalidate_recordset(fnames=['member_ids'])
self.assertEqual(user_team_leads.member_ids.ids, [self.env.user.id])
self.env['crm.lead'].create([{
'name': 'LeadOurTeam',
'team_id': user_team_leads.id,
@ -781,11 +884,10 @@ class TestCRMLead(TestCrmCommon):
for team in teams:
with self.subTest(team=team):
team_mail = f"{team.alias_name}@{team.alias_domain}"
if team != team_other_comp:
self.assertIn(f"<a href='mailto:{team_mail}'>{team_mail}</a>", self.env['crm.lead'].sudo().get_empty_list_help(""))
self.assertIn(f"<a href='mailto:{team.alias_email}'>{team.alias_email}</a>", self.env['crm.lead'].sudo().get_empty_list_help(""))
else:
self.assertNotIn(f"<a href='mailto:{team_mail}'>{team_mail}</a>", self.env['crm.lead'].sudo().get_empty_list_help(""))
self.assertNotIn(f"<a href='mailto:{team.alias_email}'>{team.alias_email}</a>", self.env['crm.lead'].sudo().get_empty_list_help(""))
team.active = False
@mute_logger('odoo.addons.mail.models.mail_thread')
@ -793,7 +895,7 @@ class TestCRMLead(TestCrmCommon):
new_lead = self.format_and_process(
INCOMING_EMAIL,
'unknown.sender@test.example.com',
'%s@%s' % (self.sales_team_1.alias_name, self.alias_domain),
self.sales_team_1.alias_email,
subject='Delivery cost inquiry',
target_model='crm.lead',
)
@ -802,49 +904,12 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(new_lead.name, 'Delivery cost inquiry')
message = new_lead.with_user(self.user_sales_manager).message_post(
body='Here is my offer !',
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_message_get_suggested_recipients(self):
"""This test checks that creating a contact from a lead with an inactive language will ignore the language
while creating a contact from a lead with an active language will take it into account """
ResLang = self.env['res.lang'].sudo().with_context(active_test=False)
# Create a lead with an inactive language -> should ignore the preset language
lang_fr = ResLang.search([('code', '=', 'fr_FR')])
if not lang_fr:
lang_fr = ResLang._create_lang('fr_FR')
# set French language as inactive then try to call "_message_get_suggested_recipients"
# -> lang code should be ignored
lang_fr.active = False
lead1 = self.env['crm.lead'].create({
'name': 'TestLead',
'email_from': self.test_email,
'lang_id': lang_fr.id,
})
data = lead1._message_get_suggested_recipients()[lead1.id]
self.assertEqual(data, [(False, self.test_email, None, 'Customer Email')])
# Create a lead with an active language -> should keep the preset language for recipients
lang_en = ResLang.search([('code', '=', 'en_US')])
if not lang_en:
lang_en = ResLang._create_lang('en_US')
# set American English language as active then try to call "_message_get_suggested_recipients"
# -> lang code should be kept
lang_en.active = True
lead2 = self.env['crm.lead'].create({
'name': 'TestLead',
'email_from': self.test_email,
'lang_id': lang_en.id,
})
data = lead2._message_get_suggested_recipients()[lead2.id]
self.assertEqual(data, [(False, self.test_email, "en_US", 'Customer Email')])
@users('user_sales_manager')
def test_phone_mobile_search(self):
@ -870,10 +935,9 @@ class TestCRMLead(TestCrmCommon):
})
# search term containing less than 3 characters should throw an error (some currently not working)
self.env['crm.lead'].search([('phone_mobile_search', 'like', '')]) # no restriction, returns all
with self.assertRaises(UserError):
self.env['crm.lead'].search([('phone_mobile_search', 'like', '')])
# with self.assertRaises(UserError):
# self.env['crm.lead'].search([('phone_mobile_search', 'like', '7 ')])
self.env['crm.lead'].search([('phone_mobile_search', 'like', '7 ')])
with self.assertRaises(UserError):
self.env['crm.lead'].search([('phone_mobile_search', 'like', 'c')])
with self.assertRaises(UserError):
@ -960,27 +1024,193 @@ class TestCRMLead(TestCrmCommon):
'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]})
lead.write({'phone': False})
self.assertFalse(lead.phone)
self.assertEqual(lead.mobile, self.test_phone_data[1])
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[1])
self.assertEqual(lead.phone_sanitized, False)
lead.write({'phone': self.test_phone_data[1], 'mobile': self.test_phone_data[2]})
lead.write({'phone': self.test_phone_data[1]})
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])
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[1])
# 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)
class TestLeadFormatAddress(FormatAddressCase):
class TestCRMLeadRotting(TestCrmCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.stage_team1_1.rotting_threshold_days = 5
cls.stage_team1_2.rotting_threshold_days = 3
@users('user_sales_manager')
def test_leads_rotting(self):
rotten_leads = self.env['crm.lead']
clean_leads = self.env['crm.lead']
close_future = datetime(2025, 1, 24, 12, 0, 0)
now = datetime(2025, 1, 20, 12, 0, 0)
close_past = datetime(2025, 1, 18, 12, 0, 0)
past = datetime(2025, 1, 10, 12, 0, 0)
last_year = datetime(2024, 1, 20, 12, 0, 0)
with self.mock_datetime_and_now(past):
rotten_leads += self.env['crm.lead'].create([
{
'name': 'Opportunity',
'type': 'opportunity',
'stage_id': self.stage_team1_1.id,
} for x in range(3)
])
rotten_leads.flush_recordset(['date_last_stage_update']) # precalculate stage update
with self.mock_datetime_and_now(close_past):
clean_leads += self.env['crm.lead'].create({
'name': "Lead that won't have time to rot",
'type': 'opportunity',
'stage_id': self.stage_team1_1.id,
})
clean_leads.flush_recordset(['date_last_stage_update']) # precalculate stage update
with self.mock_datetime_and_now(last_year):
clean_leads += self.env['crm.lead'].create({
'name': 'Opportuniy in Won Stage',
'type': 'opportunity',
'stage_id': self.stage_gen_won.id,
})
clean_leads.flush_recordset(['date_last_stage_update']) # precalculate stage update
with self.mock_datetime_and_now(now):
for lead in rotten_leads:
self.assertTrue(lead.is_rotting)
self.assertEqual(lead.rotting_days, 10)
for lead in clean_leads:
self.assertFalse(lead.is_rotting)
self.assertEqual(lead.rotting_days, 0)
rotten_leads_iterator = iter(rotten_leads)
lead_edited = next(rotten_leads_iterator)
lead_edited.name = 'Edited Opportunity'
self.assertTrue(
lead_edited.is_rotting,
'Editing the lead has no effect on rotting status',
)
lead_changed_stage = next(rotten_leads_iterator)
lead_changed_stage.stage_id = self.stage_team1_2.id
self.assertFalse(
lead_changed_stage.is_rotting,
'Changing the stage disables rotting status',
)
lead_changed_rotting_threshold = next(rotten_leads_iterator)
old_rotting_threshold = self.stage_team1_1.rotting_threshold_days
self.stage_team1_1.rotting_threshold_days = 50
self.assertFalse(
lead_changed_rotting_threshold.is_rotting,
'Changing the rotting threshold to a higher value does affect rotten leads\' status',
)
self.stage_team1_1.rotting_threshold_days = old_rotting_threshold # Revert rotting threshold
self.assertTrue(
lead_changed_rotting_threshold.is_rotting,
'Changing the threshold back should affect the status again',
)
self.stage_team1_1.rotting_threshold_days = 0
self.assertFalse(
lead_changed_rotting_threshold.is_rotting,
'A 0-day rotting threshold disables rotting',
)
self.stage_team1_1.rotting_threshold_days = old_rotting_threshold
# create a new lead in the New stage
jan20_lead = self.env['crm.lead'].create({
'name': 'Fresh Opportuniy',
'type': 'opportunity',
'stage_id': self.stage_team1_1.id,
})
# 4 days later:
with self.mock_datetime_and_now(close_future):
rotten_leads.invalidate_recordset(['is_rotting', 'rotting_days'])
self.assertEqual(
lead_changed_rotting_threshold.rotting_days,
14,
'Since this lead has not seen a stage change, it has been rotting for 14 days total',
)
self.assertFalse(
jan20_lead.is_rotting,
'Since this lead remained in a stage with a higher threshold, it\'s not rotting yet',
)
self.assertTrue(
lead_changed_stage.is_rotting,
'As its new stage has a lower rotting threshold, this lead should be rotting 3 days after its last stage change',
)
self.assertEqual(lead_changed_stage.rotting_days, 4)
def test_search_leads_rotting(self):
"""
This test checks that the result of search_leads_rotting accurately matches is_rotting computation results
"""
past = datetime(2025, 1, 1)
now = datetime(2025, 1, 10)
with self.mock_datetime_and_now(past):
all_leads = self.env['crm.lead'].create([{
'name': 'TestLead Rotting opportunity',
'type': 'opportunity',
'stage_id': self.stage_team1_1.id,
}] * 5 + [{
'name': 'TestLead Lead',
'type': 'lead',
'stage_id': self.stage_team1_1.id,
}] * 3 + [{
'name': 'TestLead Won Opportunity',
'type': 'opportunity',
'stage_id': self.stage_gen_won.id,
}] * 4)
all_leads.flush_recordset(['date_last_stage_update'])
rotten_leads = all_leads.filtered(lambda lead: 'Rotting' in lead.name)
clean_leads = all_leads - rotten_leads
with self.mock_datetime_and_now(now):
rot = self.env['crm.lead'].search([
('name', 'ilike', 'TestLead'),
('is_rotting', '=', True),
], order='id ASC')
norot = self.env['crm.lead'].search([
('name', 'ilike', 'TestLead'),
('is_rotting', '=', False),
], order='id ASC')
self.assertEqual(rot, rotten_leads)
self.assertEqual(norot, clean_leads)
@tagged('lead_internals')
class TestLeadFormTools(FormatAddressCase):
def test_address_view(self):
self.env.company.country_id = self.env.ref('base.us')
self.assertAddressView('crm.lead')
@tagged('lead_internals', 'is_query_count')
class TestCrmLeadMailTrackingDuration(MailTrackingDurationMixinCase):
@classmethod
def setUpClass(cls):
super().setUpClass('crm.lead')
def test_crm_lead_mail_tracking_duration(self):
self._test_record_duration_tracking()
def test_crm_lead_mail_tracking_duration_batch(self):
self._test_record_duration_tracking_batch()
def test_crm_lead_queries_batch_mail_tracking_duration(self):
self._test_queries_batch_duration_tracking()

View file

@ -3,7 +3,8 @@
import random
from datetime import datetime
from ast import literal_eval
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from unittest.mock import patch
@ -11,6 +12,7 @@ from odoo import fields
from odoo.addons.crm.tests.common import TestLeadConvertCommon
from odoo.tests.common import tagged
from odoo.tools import mute_logger
from odoo.fields import Datetime
class TestLeadAssignCommon(TestLeadConvertCommon):
@ -142,9 +144,9 @@ class TestLeadAssign(TestLeadAssignCommon):
count=14,
suffix='Existing')
self.assertEqual(existing_leads.team_id, self.sales_team_1, "Team should have lower sequence")
existing_leads[0].active = False # lost
existing_leads[1].probability = 100 # not won
existing_leads[2].probability = 0 # not lost
existing_leads[0].action_set_lost() # lost
existing_leads[1].probability = 100 # not won as stage is not won.
existing_leads[2].probability = 0 # not lost as active
existing_leads.flush_recordset()
self.members.invalidate_model(['lead_month_count'])
@ -160,8 +162,9 @@ class TestLeadAssign(TestLeadAssignCommon):
# sales_team_1_m2 is opt-out (new field in 14.3) -> even with max, no lead assigned
self.sales_team_1_m2.update({'assignment_max': 45, 'assignment_optout': True})
self.sales_team_1_m3.update({'assignment_max': 45})
with self.with_user('user_sales_manager'):
teams_data, members_data = self.sales_team_1._action_assign_leads(work_days=4)
teams_data, members_data = self.sales_team_1._action_assign_leads(force_quota=True)
Leads = self.env['crm.lead']
@ -175,8 +178,12 @@ class TestLeadAssign(TestLeadAssignCommon):
['TestLeadInitial_0003']
)
# TestLeadInitial_0007 has same partner as TestLeadInitial_0003
self.assertEqual(len(teams_data[self.sales_team_1]['duplicates']), 1)
# TestLeadInitial_0005 had a 0 auto_proba when its proba was set to 0.
# Therefore, it is auto_proba. At this point, its proba is 9x.xx %, and it is selected.
# These are the two leads with the highest probabilities, as they are sorted before assignment.
self.assertEqual(
sorted(members_data[self.sales_team_1_m3]['assigned'].mapped('name')),
['TestLeadInitial_0000', 'TestLeadInitial_0005']
@ -186,16 +193,16 @@ class TestLeadAssign(TestLeadAssignCommon):
self.members.invalidate_model(['lead_month_count'])
self.assertEqual(self.sales_team_1_m1.lead_month_count, 0) # archived do not get leads
self.assertEqual(self.sales_team_1_m2.lead_month_count, 0) # opt-out through assignment_max = 0
self.assertEqual(self.sales_team_1_m3.lead_month_count, 14) # 15 max on 4 days (2) + existing 12
self.assertEqual(self.sales_team_1_m3.lead_month_count, 14) # ignore actual quota (round(45/30) => +2) + existing 12
with self.with_user('user_sales_manager'):
self.env['crm.team'].browse(self.sales_team_1.ids)._action_assign_leads(work_days=4)
self.env['crm.team'].browse(self.sales_team_1.ids)._action_assign_leads(force_quota=True)
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertEqual(self.sales_team_1_m1.lead_month_count, 0) # archived do not get leads
self.assertEqual(self.sales_team_1_m2.lead_month_count, 0) # opt-out through assignment_max = 0
self.assertEqual(self.sales_team_1_m3.lead_month_count, 16) # 15 max on 4 days (2) + existing 14 and not capped anymore
self.assertEqual(self.sales_team_1_m3.lead_month_count, 16) # ignore actual quota (round(45/30) => +2) + existing 14 and not capped anymore
@mute_logger('odoo.models.unlink')
def test_assign_duplicates(self):
@ -224,7 +231,7 @@ class TestLeadAssign(TestLeadAssignCommon):
leads.flush_recordset()
with self.with_user('user_sales_manager'):
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2)
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads()
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
@ -238,12 +245,12 @@ class TestLeadAssign(TestLeadAssignCommon):
self.assertEqual(len(leads_st1) + len(leads_stc), len(leads)) # Make sure all lead are assigned
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 11) # 45 max on 2 days (3) + compensation (8.4)
self.assertMemberAssign(self.sales_team_1_m2, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_1_m3, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_convert_m1, 8) # 30 max on 15 (2) + compensation (5.6)
self.assertMemberAssign(self.sales_team_convert_m2, 15) # 60 max on 15 (4) + compsantion (11.2)
self.members.invalidate_model(['lead_month_count', 'lead_day_count'])
self.assertMemberAssign(self.sales_team_1_m1, 2) # 45 max on one month -> 2 daily
self.assertMemberAssign(self.sales_team_1_m2, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_1_m3, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m1, 1) # 30 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m2, 2) # 60 max on one month -> 2 daily
# teams assign: everything should be done due to duplicates
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
@ -281,7 +288,7 @@ class TestLeadAssign(TestLeadAssignCommon):
leads.flush_recordset()
with self.with_user('user_sales_manager'):
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2)
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads()
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
@ -295,18 +302,18 @@ class TestLeadAssign(TestLeadAssignCommon):
self.assertEqual(len(leads_st1) + len(leads_stc), len(leads)) # Make sure all lead are assigned
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 11) # 45 max on 2 days (3) + compensation (8.4)
self.assertMemberAssign(self.sales_team_1_m2, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_1_m3, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_convert_m1, 8) # 30 max on 15 (2) + compensation (5.6)
self.assertMemberAssign(self.sales_team_convert_m2, 15) # 60 max on 15 (4) + compensation (11.2)
self.members.invalidate_model(['lead_month_count', 'lead_day_count'])
self.assertMemberAssign(self.sales_team_1_m1, 2) # 45 max on one month -> 2 daily
self.assertMemberAssign(self.sales_team_1_m2, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_1_m3, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m1, 1) # 30 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m2, 2) # 60 max on one month -> 2 daily
@mute_logger('odoo.models.unlink')
def test_assign_populated(self):
""" Test assignment on a more high volume oriented test set in order to
test more real life use cases. """
# fix the seed and avoid randomness (funny: try 1870)
# fix the seed and avoid randomness
random.seed(1871)
# create leads enough to assign one month of work
@ -367,7 +374,7 @@ class TestLeadAssign(TestLeadAssignCommon):
leads.flush_recordset()
with self.with_user('user_sales_manager'):
self.env['crm.team'].browse(sales_teams.ids)._action_assign_leads(work_days=30)
self.env['crm.team'].browse(sales_teams.ids)._action_assign_leads()
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)])
@ -379,23 +386,80 @@ class TestLeadAssign(TestLeadAssignCommon):
leads_st1 = leads.filtered_domain([('team_id', '=', self.sales_team_1.id)])
leads_st2 = leads.filtered_domain([('team_id', '=', self.sales_team_convert.id)])
leads_st3 = leads.filtered_domain([('team_id', '=', sales_team_3.id)])
self.assertLessEqual(len(leads_st1), 225) # 75 * 600 / 300 * 1.5 (because random)
self.assertLessEqual(len(leads_st2), 270) # 90 * 600 / 300 * 1.5 (because random)
self.assertLessEqual(len(leads_st3), 405) # 135 * 600 / 300 * 1.5 (because random)
self.assertGreaterEqual(len(leads_st1), 75) # 75 * 600 / 300 * 0.5 (because random)
self.assertGreaterEqual(len(leads_st2), 90) # 90 * 600 / 300 * 0.5 (because random)
self.assertGreaterEqual(len(leads_st3), 135) # 135 * 600 / 300 * 0.5 (because random)
self.assertEqual(len(leads_st1), 170)
self.assertEqual(len(leads_st2), 116)
self.assertEqual(len(leads_st3), 314)
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 45) # 45 max on one month
self.assertMemberAssign(self.sales_team_1_m2, 15) # 15 max on one month
self.assertMemberAssign(self.sales_team_1_m3, 15) # 15 max on one month
self.assertMemberAssign(self.sales_team_convert_m1, 30) # 30 max on one month
self.assertMemberAssign(self.sales_team_convert_m2, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m1, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m2, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m3, 15) # 15 max on one month
self.members.invalidate_model(['lead_month_count', 'lead_day_count'])
self.assertMemberAssign(self.sales_team_1_m1, 2) # 45 max on one month -> 2 daily
self.assertMemberAssign(self.sales_team_1_m2, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_1_m3, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m1, 1) # 30 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m2, 2) # 60 max on one month -> 2 daily
self.assertMemberAssign(sales_team_3_m1, 2) # 60 max on one month -> 2 daily
self.assertMemberAssign(sales_team_3_m2, 2) # 60 max on one month -> 2 daily
self.assertMemberAssign(sales_team_3_m3, 1) # 15 max on one month -> 1 daily
def test_assign_preferred_domain(self):
""" Test preferred domain use """
random.seed(1914)
preferred_tag = self.env['crm.tag'].create({'name': 'preferred'})
leads = self._create_leads_batch(
lead_type='lead',
user_ids=[False],
count=11,
)
leads[:8].write({'tag_ids': [(6, 0, preferred_tag.ids)]})
# commit probability and related fields
leads.flush_recordset()
self.assertInitialData()
test_sales_team = self.env['crm.team'].create({
'name': 'Sales Team 5',
'sequence': 15,
'alias_name': False,
'use_leads': True,
'use_opportunities': True,
'company_id': False,
'user_id': False,
})
test_sales_team_m1 = self.env['crm.team.member'].create({
'user_id': self.user_sales_manager.id,
'crm_team_id': test_sales_team.id,
'assignment_max': 150,
'assignment_domain': False,
'assignment_domain_preferred': "[('tag_ids', 'in', %s)]" % preferred_tag.ids,
})
test_sales_team_m2 = self.env['crm.team.member'].create({
'user_id': self.user_sales_leads.id,
'crm_team_id': test_sales_team.id,
'assignment_max': 150,
'assignment_domain': False,
'assignment_domain_preferred': False,
})
test_sales_team_m3 = self.env['crm.team.member'].create({
'user_id': self.user_sales_salesman.id,
'crm_team_id': test_sales_team.id,
'assignment_max': 150,
'assignment_domain': False,
'assignment_domain_preferred': False,
})
test_sales_team._action_assign_leads()
member_leads = self.env['crm.lead'].search([
('user_id', '=', test_sales_team_m1.user_id.id),
('team_id', '=', test_sales_team_m1.crm_team_id.id),
('date_open', '>=', Datetime.now() - timedelta(hours=24)),
])
self.assertEqual(
member_leads.filtered_domain(literal_eval(test_sales_team_m1.assignment_domain_preferred)),
member_leads
)
self.assertMemberAssign(test_sales_team_m1, 5)
self.assertMemberAssign(test_sales_team_m2, 3)
self.assertMemberAssign(test_sales_team_m3, 3)
def test_assign_quota(self):
""" Test quota computation """
@ -403,26 +467,9 @@ class TestLeadAssign(TestLeadAssignCommon):
# quota computation without existing leads
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=1),
10,
"Assignment quota: 45 max on 1 days -> 1.5, compensation (45-1.5)/5 -> 8.7"
)
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=2),
11,
"Assignment quota: 45 max on 2 days -> 3, compensation (45-3)/5 -> 8.4"
)
# quota should not exceed maximum
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=30),
45,
"Assignment quota: no compensation as exceeding monthly count"
)
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=60),
90,
"Assignment quota: no compensation and no limit anymore (do as asked)"
self.sales_team_1_m1._get_assignment_quota(),
2,
"Assignment quota: 45 max -> 2 daily (round(45/30))"
)
# create exiting leads for user_sales_leads (sales_team_1_m1)
@ -433,31 +480,20 @@ class TestLeadAssign(TestLeadAssignCommon):
self.assertEqual(existing_leads.team_id, self.sales_team_1, "Team should have lower sequence")
existing_leads.flush_recordset()
self.sales_team_1_m1.invalidate_model(['lead_month_count'])
self.sales_team_1_m1.invalidate_model(['lead_month_count', 'lead_day_count'])
self.assertEqual(self.sales_team_1_m1.lead_month_count, 30)
self.assertEqual(self.sales_team_1_m1.lead_day_count, 30)
# quota computation with existing leads
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=1),
4,
"Assignment quota: 45 max on 1 days -> 1.5, compensation (45-30-1.5)/5 -> 2.7"
self.sales_team_1_m1._get_assignment_quota(),
-28,
"Assignment quota: 45 max -> 2 daily ; 30 daily lead already assign -> 2 - 30 -> -28"
)
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=2),
5,
"Assignment quota: 45 max on 2 days -> 3, compensation (45-30-3)/5 -> 2.4"
)
# quota should not exceed maximum
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=30),
45,
"Assignment quota: no compensation and no limit anymore (do as asked even with 30 already assigned)"
)
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=60),
90,
"Assignment quota: no compensation and no limit anymore (do as asked even with 30 already assigned)"
self.sales_team_1_m1._get_assignment_quota(True),
2,
"Assignment quota: 45 max ignoring existing daily lead -> 2"
)
def test_assign_specific_won_lost(self):
@ -480,8 +516,9 @@ class TestLeadAssign(TestLeadAssignCommon):
# commit probability and related fields
leads.flush_recordset()
self.sales_team_1.crm_team_member_ids.write({'assignment_max': 45})
with self.with_user('user_sales_manager'):
self.env['crm.team'].browse(self.sales_team_1.ids)._action_assign_leads(work_days=4)
self.env['crm.team'].browse(self.sales_team_1.ids)._action_assign_leads()
self.assertEqual(leads[0].team_id, self.env['crm.team'], 'Won lead should not be assigned')
self.assertEqual(leads[0].user_id, self.env['res.users'], 'Won lead should not be assigned')
@ -493,6 +530,26 @@ class TestLeadAssign(TestLeadAssignCommon):
self.assertEqual(leads[5].team_id, self.sales_team_convert, 'Assigned lead should not be reassigned')
self.assertEqual(leads[5].user_id, self.user_sales_manager, 'Assigned lead should not be reassigned')
def test_assign_team_and_salesperson_on_duplicate_lead(self):
"""Ensure leads duplicated from an existing lead are assigned correctly."""
duplicate_lead = self.env['crm.lead'].create({
'name': 'Test Lead',
'type': 'opportunity',
'probability': 15,
'partner_id': self.contact_1.id,
'team_id': False,
'user_id': False,
}).copy()
self.assertFalse(duplicate_lead.date_open)
sales_team = self.sales_team_1
sales_team.assignment_domain = [('user_id', '=', False)]
with self.with_user('user_sales_manager'):
sales_team._action_assign_leads()
self.assertEqual(duplicate_lead.team_id, sales_team)
self.assertTrue(duplicate_lead.user_id)
@mute_logger('odoo.models.unlink')
def test_merge_assign_keep_master_team(self):
""" Check existing opportunity keep its team and salesman when merged with a new lead """
@ -530,7 +587,7 @@ class TestLeadAssign(TestLeadAssignCommon):
'user_id': False,
})
sales_team_dupe._action_assign_leads(work_days=2)
sales_team_dupe._action_assign_leads()
self.assertFalse(dupe_lead.exists())
self.assertEqual(master_opp.team_id, self.sales_team_1, 'Opportunity: should keep its sales team')
self.assertEqual(master_opp.user_id, self.user_sales_manager, 'Opportunity: should keep its salesman')
@ -548,18 +605,17 @@ class TestLeadAssign(TestLeadAssignCommon):
'name': 'Sales Team 4',
'sequence': 15,
'use_leads': True,
})
})
sales_team_4_m1 = self.env['crm.team.member'].create({
'user_id': self.user_sales_salesman.id,
'crm_team_id': sales_team_4.id,
'assignment_max': 30,
})
sales_team_4_m1.lead_month_count = 50
sales_team_4_m1.lead_month_count = 30
sales_team_4_m1.lead_day_count = 2
leads.team_id = sales_team_4.id
members_data = sales_team_4_m1._assign_and_convert_leads(work_days=0.2)
self.assertEqual(
len(members_data[sales_team_4_m1]['assigned']),
0,
members_data = sales_team_4._assign_and_convert_leads()
self.assertFalse(members_data,
"If team member has lead count greater than max assign,then do not assign any more")

View file

@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from itertools import product
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
from odoo.tests import Form, tagged, users
@tagged('lead_manage')
class TestLeadConvertForm(crm_common.TestLeadConvertCommon):
@ -59,13 +57,13 @@ class TestLeadConvertForm(crm_common.TestLeadConvertCommon):
@tagged('lead_manage')
class TestLeadConvert(crm_common.TestLeadConvertCommon):
"""
TODO: created partner (handle assignation) has team of lead
TODO: created partner (handle assignment) has team of lead
TODO: create partner has user_id coming from wizard
"""
@classmethod
def setUpClass(cls):
super(TestLeadConvert, cls).setUpClass()
super().setUpClass()
date = Datetime.from_string('2020-01-20 16:00:00')
cls.crm_lead_dt_mock.now.return_value = date
@ -80,6 +78,11 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
'email_from': test_lead.email_from,
'probability': 0, 'active': False,
},
{'name': 'Duplicate lead: same email_from, archived (not lost)',
'type': 'lead',
'email_from': test_lead.email_from,
'probability': 50, 'active': False,
},
{'name': 'Duplicate lead: same email_from, proba 0 but not lost',
'type': 'lead',
'email_from': test_lead.email_from,
@ -94,37 +97,50 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
'type': 'opportunity',
'email_from': test_lead.email_from,
'probability': 100, 'stage_id': self.stage_team1_2.id,
},
{'name': 'Duplicate opp: same email_from, archived (not lost)',
'type': 'opportunity',
'email_from': test_lead.email_from,
'probability': 50, 'stage_id': self.stage_team1_2.id,
'active': False,
}
])
lead_lost = dup_leads.filtered(lambda lead: lead.name == 'Duplicate lead: same email_from, lost')
_opp_proba100 = dup_leads.filtered(lambda lead: lead.name == 'Duplicate opp: same email_from, proba 100 but not won')
opp_won = dup_leads.filtered(lambda lead: lead.name == 'Duplicate opp: same email_from, won')
opp_lost = dup_leads.filtered(lambda lead: lead.name == 'Duplicate: lost opportunity')
self.assertEqual(len(dup_leads), 10, 'Be sure below quick access are relevant')
opp_lost = dup_leads[3]
lead_lost = dup_leads[4]
lead_archived = dup_leads[5]
opp_won = dup_leads[7]
_opp_proba100 = dup_leads[8]
opp_archived = dup_leads[9]
test_lead.write({'partner_id': customer.id})
# not include_lost = remove archived leads as well as 'won' opportunities
# not include_lost = remove archived leads/opps, lost leads/opps as well
# as 'won' opportunities
result = test_lead._get_lead_duplicates(
partner=test_lead.partner_id,
email=test_lead.email_from,
include_lost=False
)
self.assertEqual(result, test_lead + dup_leads - (lead_lost + opp_won + opp_lost))
self.assertEqual(
result,
test_lead + dup_leads - (lead_lost + lead_archived + opp_won + opp_archived + opp_lost),
'Should not include: lost lead or opp, archived lead / opp (aka: only active lead/opp not won nor lost)'
)
# include_lost = remove archived opp only
# include_lost = remove archived lead only (archived opp is ok)
result = test_lead._get_lead_duplicates(
partner=test_lead.partner_id,
email=test_lead.email_from,
include_lost=True
include_lost=True,
)
self.assertEqual(result, test_lead + dup_leads - (lead_lost))
self.assertEqual(result, test_lead + dup_leads - (lead_lost + lead_archived + opp_won))
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.lang_id, self.lang_fr)
self.assertFalse(self.lead_1.mobile)
self.assertEqual(self.lead_1.phone, '+1 202 555 9999')
self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
self.assertEqual(self.lead_1.team_id, self.sales_team_1)
@ -149,7 +165,6 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
self.assertEqual(lead.partner_id, self.contact_2)
self.assertEqual(lead.email_from, self.contact_2.email)
self.assertEqual(lead.lang_id, self.lang_en)
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)
@ -317,24 +332,6 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
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
@ -396,19 +393,11 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
}]
self.lead_1.convert_opportunity(False)
self.assertEqual(self.lead_1.team_id, initial_team)
self.assertEqual(self.lead_1.lead_properties, [{
'name': 'test',
'type': 'char',
'value': 'test value',
}])
self.assertEqual(self.lead_1.lead_properties, {'test': 'test value'})
# re-writing the team, but keeping the same value should not reset the properties
self.lead_1.write({'team_id': self.lead_1.team_id.id})
self.assertEqual(self.lead_1.lead_properties, [{
'name': 'test',
'type': 'char',
'value': 'test value',
}])
self.assertEqual(self.lead_1.lead_properties, {'test': 'test value'})
@users('user_sales_manager')
def test_lead_convert_properties_reset(self):
@ -424,6 +413,58 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
self.assertNotEqual(self.lead_1.team_id, initial_team)
self.assertFalse(self.lead_1.lead_properties)
@users('user_sales_manager')
def test_lead_convert_wizard_new_partner(self):
no_partner = self.env['res.partner']
test_partner_lead, test_partner_wizard, commercial_partner = self.env['res.partner'].create([
{'name': 'Lead Test Partner'},
{'name': 'Wizard Test Partner'},
{'name': 'Company Partner', 'is_company': True},
])
case_values = product(
[no_partner, test_partner_lead],
[False, 'New Company'],
[no_partner, commercial_partner],
[no_partner, test_partner_wizard],
['create', 'exist'],
)
for (lead_partner, lead_company_name, wizard_company, wizard_contact, wizard_action) in case_values:
(test_partner_lead + test_partner_wizard).parent_id = False
commercial_partner.invalidate_recordset()
lead_contact_name = lead_partner.name or 'Test Contact Name'
lead = self.env['crm.lead'].create({
'name': 'Test Lead',
'contact_name': lead_contact_name,
'partner_id': lead_partner.id,
'partner_name': lead_company_name,
})
wizard = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': lead.id,
'active_ids': lead.ids,
}).create({})
wizard.write({'action': wizard_action, 'name': 'convert'})
if wizard_contact:
wizard.partner_id = wizard_contact
if wizard_company:
wizard.commercial_partner_id = wizard_company
with self.subTest(
lead_company_name=lead_company_name, lead_partner=lead_partner.name,
wizard_company=wizard_company.name, wizard_contact=wizard_contact.name, wizard_action=wizard_action
):
wizard.action_apply()
self.assertEqual(lead.type, 'opportunity')
self.assertEqual(bool(lead.partner_id), bool(wizard_action == 'create' or lead_partner or wizard_contact))
if wizard_action == 'exist' and (lead_partner or wizard_contact):
self.assertEqual(lead.partner_id, wizard_contact or lead_partner)
if wizard_action == 'create' and not lead_partner and not wizard_contact and wizard_company:
self.assertTrue(lead.partner_id)
self.assertEqual(lead.partner_id.name, lead_contact_name)
self.assertEqual(lead.partner_id.parent_id, wizard_company)
if wizard_action == 'create' and (wizard_contact or lead_partner):
self.assertEqual(lead.partner_id, wizard_contact or lead_partner)
self.assertFalse(lead.partner_id.parent_id)
@users('user_sales_manager')
def test_lead_merge(self):
""" Test convert wizard working in merge mode """

View file

@ -15,6 +15,13 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
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
def setUp(self):
super().setUp()
# patch registry to simulate a ready environment
self.patch(self.env.registry, 'ready', True)
# we don't use mock_mail_gateway thus want to mock smtp to test the stack
self._mock_smtplib_connection()
@users('user_sales_manager')
def test_assignment_salesmen(self):
test_leads = self._create_leads_batch(count=50, user_ids=[False])
@ -24,7 +31,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
with self.assertQueryCount(user_sales_manager=0):
test_leads = self.env['crm.lead'].browse(test_leads.ids)
with self.assertQueryCount(user_sales_manager=543): # crm 537 / com 543 / ent 537
with self.assertQueryCount(user_sales_manager=531): # crm 605 / com 605 / ent 605
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)
@ -42,7 +49,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
with self.assertQueryCount(user_sales_manager=0):
test_leads = self.env['crm.lead'].browse(test_leads.ids)
with self.assertQueryCount(user_sales_manager=524): # crm 521 / com 524
with self.assertQueryCount(user_sales_manager=483): # crm 544 / com 546 / ent 585
test_leads._handle_salesmen_assignment(user_ids=user_ids, team_id=team_id)
self.assertEqual(test_leads.team_id, self.sales_team_convert)
@ -89,7 +96,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
self.assertEqual(new_partner.name, 'Amy Wong')
self.assertEqual(new_partner.email, 'amy.wong@test.example.com')
# test unforced assignation
# test unforced assignment
mass_convert.write({
'user_ids': self.user_sales_salesman.ids,
})
@ -167,7 +174,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
user_ids = self.assign_users.ids
# randomness: at least 1 query
with self.assertQueryCount(user_sales_manager=1704): # crm 1410 / com 1697
with self.assertQueryCount(user_sales_manager=1442): # crm ??
mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
'active_model': 'crm.lead',
'active_ids': test_leads.ids,
@ -211,3 +218,20 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
self.assertEqual(lead.type, 'opportunity')
assigned_user = self.assign_users[idx % len(self.assign_users)]
self.assertEqual(lead.user_id, assigned_user)
@users('user_sales_manager')
def test_mass_convert_with_original_and_duplicate_selected(self):
_customer, lead_1_dups = self._create_duplicates(self.lead_1, create_opp=False)
mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
'active_model': 'crm.lead',
'active_ids': (self.lead_1 + lead_1_dups).ids,
}).create({
'deduplicate': True,
})
mass_convert.action_mass_convert()
remaining_leads = (self.lead_1 + lead_1_dups).exists()
self.assertEqual(len(remaining_leads), 1)
self.assertEqual(remaining_leads.type, 'opportunity')

View file

@ -2,227 +2,226 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.addons.iap.tools import iap_tools
from odoo.tests.common import tagged, users
@tagged('lead_manage')
class TestLeadConvert(TestCrmCommon):
@tagged('lead_internals')
class TestCRMLead(TestCrmCommon):
@users('user_sales_manager')
def test_potential_duplicates(self):
company = self.env['res.partner'].create({
'name': 'My company',
'email': 'mycompany@company.com',
'is_company': True,
'street': '57th Street',
@classmethod
def setUpClass(cls):
super().setUpClass()
# To avoid magic phone sanitization
cls.env.company.country_id = cls.env.ref('base.us')
cls.emails_provider_generic = {
('robert.poilvert@gmail.com', 'robert.poilvert@gmail.com'),
('fp@odoo.com', 'fp@odoo.com'),
('fp.alias@mail.odoo.com', 'fp.alias@mail.odoo.com'),
}
cls.emails_provider_company = {
('robert.poilvert@mycompany.com', 'mycompany.com'),
('fp@subdomain.odoo.com', 'subdomain.odoo.com'),
}
# customer data
country_us_id = cls.env.ref('base.us').id
cls.test_company = cls.env['res.partner'].create({
'city': 'New New York',
'country_id': self.env.ref('base.us').id,
'country_id': country_us_id,
'email': 'test.company@another.email.company.com',
'is_company': True,
'name': 'My company',
'street': '57th Street',
'zip': '12345',
})
cls.test_partners = cls.env['res.partner'].create([
{
'city': 'New York',
'country_id': country_us_id,
'email': 'dave@another.email.company.com',
'is_company': False,
'name': 'Dave',
'phone': '+1 202 000 0123',
'parent_id': cls.test_company.id,
'street': 'Pearl street',
'zip': '12345',
},
{
'city': 'New York',
'country_id': country_us_id,
'email': 'eve@another.email.company.com',
'is_company': False,
'name': 'Eve',
'parent_id': cls.test_company.id,
'phone': '+1 202 000 3210',
'street': 'Wall street',
'zip': '12345',
}
])
partner_1 = self.env['res.partner'].create({
'name': 'Dave',
'email': 'dave@odoo.com',
'mobile': '+1 202 555 0123',
'phone': False,
'parent_id': company.id,
'is_company': False,
'street': 'Pearl street',
'city': 'California',
'country_id': self.env.ref('base.us').id,
'zip': '95826',
})
partner_2 = self.env['res.partner'].create({
'name': 'Eve',
'email': 'eve@odoo.com',
'mobile': '+1 202 555 3210',
'phone': False,
'parent_id': company.id,
'is_company': False,
'street': 'Wall street',
'city': 'New York',
'country_id': self.env.ref('base.us').id,
'zip': '54321',
})
lead_1 = self.env['crm.lead'].create({
'name': 'Lead 1',
# base leads on which duplicate detection is performed
cls.lead_generic = cls.env['crm.lead'].create({
'country_id': country_us_id,
'email_from': 'FP@odoo.com',
'name': 'Generic 1',
'partner_id': cls.test_partners[0].id,
'phone': '+1 202 555 0123',
'type': 'lead',
'partner_name': 'Alice',
'email_from': 'alice@odoo.com',
})
lead_2 = self.env['crm.lead'].create({
'name': 'Opportunity 1',
'type': 'opportunity',
'email_from': 'alice@odoo.com',
})
lead_3 = self.env['crm.lead'].create({
'name': 'Opportunity 2',
'type': 'opportunity',
'email_from': 'alice@odoo.com',
})
lead_4 = self.env['crm.lead'].create({
'name': 'Lead 2',
'type': 'lead',
'partner_name': 'Alice Doe'
})
lead_5 = self.env['crm.lead'].create({
'name': 'Opportunity 3',
'type': 'opportunity',
'partner_name': 'Alice Doe'
})
lead_6 = self.env['crm.lead'].create({
'name': 'Opportunity 4',
'type': 'opportunity',
'partner_name': 'Bob Doe'
})
lead_7 = self.env['crm.lead'].create({
'name': 'Opportunity 5',
'type': 'opportunity',
'partner_name': 'Bob Doe',
'email_from': 'bob@odoo.com',
})
lead_8 = self.env['crm.lead'].create({
'name': 'Opportunity 6',
'type': 'opportunity',
'email_from': 'bob@mymail.com',
})
lead_9 = self.env['crm.lead'].create({
'name': 'Opportunity 7',
'type': 'opportunity',
'email_from': 'alice@mymail.com',
})
lead_10 = self.env['crm.lead'].create({
'name': 'Opportunity 8',
'type': 'opportunity',
'probability': 0,
'active': False,
'email_from': 'alice@mymail.com',
})
lead_11 = self.env['crm.lead'].create({
'name': 'Opportunity 9',
'type': 'opportunity',
'contact_name': 'charlie'
})
lead_12 = self.env['crm.lead'].create({
'name': 'Opportunity 10',
'type': 'opportunity',
'contact_name': 'Charlie Chapelin',
})
lead_13 = self.env['crm.lead'].create({
'name': 'Opportunity 8',
'type': 'opportunity',
'partner_id': partner_1.id
})
lead_14 = self.env['crm.lead'].create({
'name': 'Lead 3',
'type': 'lead',
'partner_id': partner_2.id
})
self.assertEqual(lead_1 + lead_2 + lead_3, lead_1.duplicate_lead_ids)
self.assertEqual(lead_1 + lead_2 + lead_3, lead_2.duplicate_lead_ids)
self.assertEqual(lead_1 + lead_2 + lead_3, lead_3.duplicate_lead_ids)
self.assertEqual(lead_4 + lead_5, lead_4.duplicate_lead_ids)
self.assertEqual(lead_4 + lead_5, lead_5.duplicate_lead_ids)
self.assertEqual(lead_6 + lead_7, lead_6.duplicate_lead_ids)
self.assertEqual(lead_6 + lead_7, lead_7.duplicate_lead_ids)
self.assertEqual(lead_8 + lead_9 + lead_10, lead_8.duplicate_lead_ids)
self.assertEqual(lead_8 + lead_9 + lead_10, lead_9.duplicate_lead_ids)
self.assertEqual(lead_8 + lead_9 + lead_10, lead_10.duplicate_lead_ids)
self.assertEqual(lead_11 + lead_12, lead_11.duplicate_lead_ids)
self.assertEqual(lead_12, lead_12.duplicate_lead_ids)
self.assertEqual(lead_13 + lead_14, lead_13.duplicate_lead_ids)
self.assertEqual(lead_13 + lead_14, lead_14.duplicate_lead_ids)
@users('user_sales_manager')
def test_potential_duplicates_with_phone(self):
customer = self.env['res.partner'].create({
'email': 'customer1@duplicate.example.com',
'mobile': '+32485001122',
'name': 'Customer1',
'phone': '(803)-456-6126',
})
base_lead = self.env['crm.lead'].create({
'name': 'Base Lead',
'partner_id': customer.id,
cls.lead_company = cls.env['crm.lead'].create({
'country_id': country_us_id,
'email_from': 'floppy@MYCOMPANY.com',
'partner_id': False,
'name': 'CompanyMail 1',
'phone': '+1 202 666 4567',
'type': 'lead',
})
self.assertEqual(base_lead.contact_name, customer.name)
self.assertEqual(base_lead.mobile, customer.mobile)
self.assertFalse(base_lead.partner_name)
self.assertEqual(base_lead.phone, customer.phone)
# duplicates
cls.lead_generic_email_dupes = cls.env['crm.lead'].create([
# email based: normalized version used for email domain criterion
{
'email_from': '"Fabulous Fab" <fp@ODOO.COM>',
'name': 'Dupe1 of fp@odoo.com (same email)',
'type': 'lead',
},
{
'email_from': 'FP@odoo.com',
'name': 'Dupe2 of fp@odoo.com (same email)',
'type': 'lead',
},
# phone_sanitized based
{
'email_from': 'not.fp@not.odoo.com',
'name': 'Dupe3 of fp@odoo.com (same phone sanitized)',
'phone': '+1 202 555 0123',
'type': 'lead',
},
{
'email_from': 'not.fp@not.odoo.com',
'phone': '+1 202 555 0123',
'name': 'Dupe4 of fp@odoo.com (same phone sanitized)',
'type': 'lead',
},
# same commercial entity
{
'name': 'Dupe5 of fp@odoo.com (same commercial entity)',
'partner_id': cls.test_partners[1].id,
},
{
'name': 'Dupe6 of fp@odoo.com (same commercial entity)',
'partner_id': cls.test_company.id,
}
])
cls.lead_generic_email_notdupes = cls.env['crm.lead'].create([
# email: check for exact match
{
'email_from': 'not.fp@odoo.com',
'name': 'NotADupe1',
'type': 'lead',
},
])
cls.lead_company_email_dupes = cls.env['crm.lead'].create([
# email based: normalized version used for email domain criterion
{
'email_from': '"The Other Fabulous Fab" <fp@mycompany.COM>',
'name': 'Dupe1 of mycompany@mycompany.com (same company)',
'type': 'lead',
},
{
'email_from': '"Same Email" <floppy@mycompany.com>',
'name': 'Dupe2 of mycompany@mycompany.com (same company)',
'type': 'lead',
},
# phone_sanitized based
{
'email_from': 'not.floppy@not.mycompany.com',
'name': 'Dupe3 of fp@odoo.com (same phone sanitized)',
'phone': '+1 202 666 4567',
'type': 'lead',
},
{
'email_from': 'not.floppy@not.mycompany.com',
'phone': '+1 202 666 4567',
'name': 'Dupe4 of fp@odoo.com (same phone sanitized)',
'type': 'lead',
},
])
cls.lead_company_email_notdupes = cls.env['crm.lead'].create([
# email: check same company
{
'email_from': 'floppy@zboing.MYCOMPANY.com',
'name': 'NotADupe2',
'type': 'lead',
},
])
dup1_1 = self.env['crm.lead'].create({
'name': 'Base Lead Dup1',
'type': 'lead',
'phone': '456-6126', # shorter version of base_lead
'mobile': ' ', # empty string shouldn't crash Odoo
'partner_name': 'Partner Name 1',
})
dup1_2 = self.env['crm.lead'].create({
'name': 'Base Lead Dup2',
'mobile': '8034566126',
'partner_name': 'Partner Name 2',
'type': 'lead',
})
dup1_3 = self.env['crm.lead'].create({
'name': 'Base Lead Dup3',
'partner_name': 'Partner Name 3',
'phone': '(803)-456-6126',
'type': 'lead',
})
dup1_4 = self.env['crm.lead'].create({
'mobile': '0032485001122',
# 'mobile': '0485001122', # note: does not work
'name': 'Base Lead Dup4',
'partner_name': 'Partner Name 4',
'phone': False,
'type': 'lead',
})
def test_assert_initial_values(self):
""" Just be sure of initial value for those tests """
lead_generic = self.lead_generic.with_env(self.env)
self.assertEqual(lead_generic.phone_sanitized, '+12025550123')
self.assertEqual(lead_generic.email_domain_criterion, 'fp@odoo.com')
self.assertEqual(lead_generic.email_normalized, 'fp@odoo.com')
expected = base_lead + dup1_2 + dup1_3 + dup1_4 # dup1_1 is shorter than lead -> not a dupe
self.assertEqual(
base_lead.duplicate_lead_ids, expected,
'CRM: missing %s, extra %s' % ((expected - base_lead.duplicate_lead_ids).mapped('name'), (base_lead.duplicate_lead_ids - expected).mapped('name'))
)
expected = base_lead + dup1_1 + dup1_2 + dup1_3 # dup1_4 has mobile of customer, but no link with dup1_1
self.assertEqual(
dup1_1.duplicate_lead_ids, expected,
'CRM: missing %s, extra %s' % ((expected - dup1_1.duplicate_lead_ids).mapped('name'), (dup1_1.duplicate_lead_ids - expected).mapped('name'))
)
lead_company = self.lead_company.with_env(self.env)
self.assertEqual(lead_company.phone_sanitized, '+12026664567')
self.assertEqual(lead_company.email_domain_criterion, '@mycompany.com')
self.assertEqual(lead_company.email_normalized, 'floppy@mycompany.com')
@users('user_sales_manager')
def test_potential_duplicates_with_invalid_email(self):
lead_1 = self.env['crm.lead'].create({
'name': 'Lead 1',
'type': 'lead',
'email_from': 'mail"1@mymail.com'
})
lead_2 = self.env['crm.lead'].create({
'name': 'Opportunity 1',
'type': 'opportunity',
'email_from': 'mail2@mymail.com'
})
lead_3 = self.env['crm.lead'].create({
'name': 'Opportunity 2',
'type': 'lead',
'email_from': 'odoo.com'
})
lead_4 = self.env['crm.lead'].create({
'name': 'Opportunity 3',
'type': 'opportunity',
'email_from': 'odoo.com'
})
lead_5 = self.env['crm.lead'].create({
'name': 'Opportunity 3',
'type': 'opportunity',
'email_from': 'myodoo.com'
})
@users('user_sales_leads')
def test_crm_lead_duplicates_fetch(self):
""" Test heuristic to find duplicates of a given lead. """
# generic provider-based email
lead_generic = self.lead_generic.with_env(self.env)
self.assertEqual(lead_1 + lead_2, lead_1.duplicate_lead_ids)
self.assertEqual(lead_1 + lead_2, lead_2.duplicate_lead_ids)
self.assertEqual(lead_3 + lead_4 + lead_5, lead_3.duplicate_lead_ids)
self.assertEqual(lead_3 + lead_4 + lead_5, lead_4.duplicate_lead_ids)
self.assertEqual(lead_5, lead_5.duplicate_lead_ids)
self.assertEqual(lead_generic.duplicate_lead_ids,
lead_generic + self.lead_generic_email_dupes,
'Duplicates: exact email matching (+ self)')
# company-based email
lead_company = self.lead_company.with_env(self.env)
self.assertEqual(lead_company.duplicate_lead_ids,
lead_company + self.lead_company_email_dupes,
'Duplicates: exact email matching (+ self)')
@users('user_sales_leads')
def test_crm_lead_email_domain_criterion(self):
""" Test computed field 'email_domain_criterion' used notably to fetch
duplicates. """
for test_email, provider in self.emails_provider_generic:
with self.subTest(test_email=test_email, provider=provider):
lead = self.env['crm.lead'].create({
'email_from': test_email,
'name': test_email,
})
self.assertEqual(lead.email_domain_criterion, provider)
for test_email, provider in self.emails_provider_company:
with self.subTest(test_email=test_email, provider=provider):
lead = self.env['crm.lead'].create({
'email_from': test_email,
'name': test_email,
})
self.assertEqual(lead.email_domain_criterion, f'@{provider}',)
@users('user_sales_leads')
def test_iap_tools(self):
""" Test iap tools specifically """
for test_email, provider in self.emails_provider_generic:
with self.subTest(test_email=test_email, provider=provider):
self.assertEqual(
iap_tools.mail_prepare_for_domain_search(test_email),
test_email,
'As provider is a generic one, complete email should be returned for a company-based mail search'
)
for test_email, provider in self.emails_provider_company:
with self.subTest(test_email=test_email, provider=provider):
self.assertEqual(
iap_tools.mail_prepare_for_domain_search(test_email),
f'@{provider}',
'As provider is a company one, only the domain part should be returned for a company-based mail search'
)

View file

@ -1,132 +0,0 @@
# -*- 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
from odoo.tools import mute_logger
@tagged('lead_manage', 'lead_lost')
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):
""" Test setting a lead as lost using the wizard. Also check that an
'html editor' void content used as feedback is not logged on the lead. """
# Initial data
self.assertEqual(len(self.lead_1.message_ids), 1, 'Should contain creation message')
creation_message = self.lead_1.message_ids[0]
self.assertEqual(creation_message.subtype_id, self.env.ref('crm.mt_lead_create'))
# Update responsible as ACLs is "own only" for user_sales_salesman
self.lead_1.with_user(self.user_sales_manager).write({
'user_id': self.user_sales_salesman.id,
'probability': 32,
})
self.flush_tracking()
lead = self.env['crm.lead'].browse(self.lead_1.ids)
self.assertFalse(lead.lost_reason_id)
self.assertEqual(lead.probability, 32)
self.assertEqual(len(lead.message_ids), 2, 'Should have tracked new responsible')
update_message = lead.message_ids[0]
self.assertEqual(update_message.subtype_id, self.env.ref('mail.mt_note'))
# mark as lost using the wizard
lost_wizard = self.env['crm.lead.lost'].with_context({
'active_ids': lead.ids,
}).create({
'lost_reason_id': self.lost_reason.id,
'lost_feedback': '<p></p>', # void content
})
lost_wizard.action_lost_reason_apply()
self.flush_tracking()
# check lead update
self.assertFalse(lead.active)
self.assertEqual(lead.automated_probability, 0)
self.assertEqual(lead.lost_reason_id, self.lost_reason) # TDE FIXME: should be called lost_reason_id non didjou
self.assertEqual(lead.probability, 0)
# check messages
self.assertEqual(len(lead.message_ids), 3, 'Should have logged a tracking message for lost lead with reason')
update_message = lead.message_ids[0]
self.assertEqual(update_message.subtype_id, self.env.ref('crm.mt_lead_lost'))
self.assertEqual(len(update_message.tracking_value_ids), 2, 'Tracking: active, lost reason')
self.assertTracking(
update_message,
[('active', 'boolean', True, False),
('lost_reason_id', 'many2one', False, self.lost_reason)
]
)
@users('user_sales_leads')
def test_lead_lost_batch_wfeedback(self):
""" Test setting leads as lost in batch using the wizard, including a log
message. """
leads = self._create_leads_batch(lead_type='lead', count=10, probabilities=[10, 20, 30])
self.assertEqual(len(leads), 10)
self.flush_tracking()
lost_wizard = self.env['crm.lead.lost'].with_context({
'active_ids': leads.ids,
}).create({
'lost_reason_id': self.lost_reason.id,
'lost_feedback': '<p>I cannot find it. It was in my closet and pouf, disappeared.</p>',
})
lost_wizard.action_lost_reason_apply()
self.flush_tracking()
for lead in leads:
# check content
self.assertFalse(lead.active)
self.assertEqual(lead.automated_probability, 0)
self.assertEqual(lead.probability, 0)
self.assertEqual(lead.lost_reason_id, self.lost_reason)
# check messages
self.assertEqual(len(lead.message_ids), 2, 'Should have 2 messages: creation, lost with log')
lost_message = lead.message_ids.filtered(lambda msg: msg.subtype_id == self.env.ref('crm.mt_lead_lost'))
self.assertTrue(lost_message)
self.assertTracking(
lost_message,
[('active', 'boolean', True, False),
('lost_reason_id', 'many2one', False, self.lost_reason)
]
)
self.assertIn('<p>I cannot find it. It was in my closet and pouf, disappeared.</p>', lost_message.body,
'Feedback should be included directly within tracking message')
@users('user_sales_salesman')
@mute_logger('odoo.addons.base.models')
def test_lead_lost_crm_rights(self):
""" Test ACLs of lost reasons management and usage """
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()

View file

@ -63,6 +63,9 @@ class TestLeadMerge(TestLeadMergeCommon):
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.probability, 20)
self.assertTrue(self.lead_1.automated_probability > 0)
self.assertFalse(self.lead_1.is_automated_probability)
self.assertEqual(self.lead_w_partner.stage_id, self.env['crm.stage'])
self.assertEqual(self.lead_w_partner.user_id, self.env['res.users'])
@ -173,9 +176,7 @@ class TestLeadMerge(TestLeadMergeCommon):
})
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)
self.assertEqual(merge.opportunity_ids, self.leads - self.lead_w_partner_company, 'Should not keep won opps')
ordered_merge = self.lead_w_contact + self.lead_w_email + self.lead_1 + self.lead_w_partner
ordered_merge_description = '<br><br>'.join(l.description for l in ordered_merge)
@ -221,8 +222,9 @@ class TestLeadMerge(TestLeadMergeCommon):
# 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, 'Even lost are included if asked by user')
# remove lost lead, otherwise limit of 5 is going to raise
merge.write({'opportunity_ids': [(3, self.lead_w_email_lost.id)]})
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
@ -284,6 +286,7 @@ class TestLeadMerge(TestLeadMergeCommon):
leads = self.env['crm.lead'].browse((self.lead_1 + self.lead_w_partner + self.lead_w_partner_company).ids)
merged_lead = self._run_merge_wizard(leads)
self.assertEqual(merged_lead, self.lead_1)
self.assertTrue(self.lead_1.automated_probability > 0)
self.assertEqual(merged_lead.probability, 0, "Manual Probability should remain the same after the merge")
self.assertFalse(merged_lead.is_automated_probability)
@ -446,7 +449,7 @@ class TestLeadMerge(TestLeadMergeCommon):
} for idx in range(4)
])
lead_1 = self.env['crm.lead'].browse(self.lead_1.ids)
activity = lead_1.activity_schedule('crm.lead_test_activity_1')
activity = lead_1.activity_schedule('crm.lead_test_activity_1', user_id=self.user_sales_manager.id)
calendar_event = self.env['calendar.event'].create({
'name': 'Meeting with partner',
'activity_ids': [(4, activity.id)],
@ -460,7 +463,9 @@ class TestLeadMerge(TestLeadMergeCommon):
# run merge and check documents are moved to the master record
merge = self.env['crm.merge.opportunity'].with_context({
'active_model': 'crm.lead',
'active_ids': self.leads.ids,
# with 'active_test' context key, lost are included, which would make 6 leads
# while 5 is maximum for manual merge -> exclude it directly
'active_ids': (self.leads - self.lead_w_email_lost).ids,
'active_id': False,
}).create({
'team_id': self.sales_team_convert.id,

View file

@ -205,6 +205,7 @@ class TestCRMLeadMultiCompany(TestCrmCommon):
# writing current user on lead would imply putting its team and team's company
# on lead (aka self.company_2), and this clashes with company restriction on
# customer
self.env.user.company_ids -= self.company_main
with self.assertRaises(UserError):
lead.write({
'user_id': self.env.user,
@ -301,22 +302,26 @@ class TestCRMLeadMultiCompany(TestCrmCommon):
self.assertEqual(crm_lead_form.company_id, self.company_2, 'Crm: company comes from partner')
def test_gateway_incompatible_company_error_on_incoming_email(self):
# set automatic assignment to Manual to circumvent automatic lead assignment,
# so as to reproduce the conditions in which the original bug appeared
self.env['ir.config_parameter'].set_param('crm.lead.auto.assignment', True)
self.assertTrue(self.sales_team_1.alias_name)
self.assertFalse(self.sales_team_1.company_id)
customer_company = self.env['res.partner'].create({
'company_id': self.company_2.id,
'email': 'customer.another.company@test.customer.com',
'mobile': '+32455000000',
'phone': '+32455000000',
'name': 'InCompany Customer',
})
new_lead = self.format_and_process(
INCOMING_EMAIL,
customer_company.email,
'%s@%s' % (self.sales_team_1.alias_name, self.alias_domain),
self.sales_team_1.alias_email,
subject='Team having partner in company',
target_model='crm.lead',
)
self.assertFalse(new_lead.user_id)
self.assertEqual(new_lead.company_id, self.company_2)
self.assertEqual(new_lead.email_from, customer_company.email)
self.assertEqual(new_lead.partner_id, customer_company)

View file

@ -1,26 +1,49 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.addons.crm.tests.common import INCOMING_EMAIL, TestCrmCommon
from odoo.tests import tagged, users
from odoo.tools import mute_logger
from odoo.tools import email_normalize, formataddr, mute_logger
@tagged('mail_thread', 'mail_gateway')
class NewLeadNotification(TestCrmCommon):
""" Test mail features support on lead + specific overrides and support """
@classmethod
def setUpClass(cls):
""" Activate some langs to test lang propagation in various mail flows """
super(NewLeadNotification, cls).setUpClass()
cls._activate_multi_company()
cls.test_email = '"Test Email" <test.email@example.com>'
model_lang = cls.env['res.lang'].sudo().with_context(active_test=False)
# Create a lead with an inactive language -> should ignore the preset language
cls.lang_fr = model_lang.search([('code', '=', 'fr_FR')])
if not cls.lang_fr:
cls.lang_fr = model_lang._create_lang('fr_FR')
# set French language as inactive then try to call "_message_get_suggested_recipients"
# -> lang code should be ignored
cls.lang_fr.active = False
# Create a lead with an active language -> should keep the preset language for recipients
cls.lang_en = model_lang.search([('code', '=', 'en_US')])
if not cls.lang_en:
cls.lang_en = model_lang._create_lang('en_US')
# set American English language as active then try to call "_message_get_suggested_recipients"
# -> lang code should be kept
cls.lang_en.active = True
@users('user_sales_manager')
def test_lead_message_get_suggested_recipient(self):
""" Test '_message_get_suggested_recipients' and its override in lead. """
def test_lead_message_get_suggested_recipients(self):
""" Test '_message_get_suggested_recipients' and its override in lead
when dealing with various emails. """
self.maxDiff = None # to ease assertDictEqual usage
company_partner = self.env['res.partner'].create(
{'name': 'test_lead_message_get_suggested_recipients_company_partner', 'is_company': True}
)
partner_no_email = self.env['res.partner'].create({'name': 'Test Partner', 'email': False})
(
lead_format,
lead_multi,
lead_from,
lead_partner,
lead_partner_no_email,
lead_partner_no_email_with_cc
) = self.env['crm.lead'].create([
leads = self.env['crm.lead'].create([
{
'email_from': '"New Customer" <new.customer.format@test.example.com>',
'name': 'Test Suggestion (email_from with format)',
@ -31,42 +54,202 @@ class NewLeadNotification(TestCrmCommon):
'name': 'Test Suggestion (email_from multi)',
'partner_name': 'Multi Name',
'user_id': self.user_sales_leads.id,
}, {
'email_from': 'new.customer.with.parent@test.example.com',
'name': 'Test Suggestion (email_from with matching partner_name)',
'partner_name': 'test_lead_message_get_suggested_recipients_company_partner',
'user_id': self.user_sales_leads.id,
}, {
'email_from': 'new.customer.simple@test.example.com',
'name': 'Test Suggestion (email_from)',
'partner_name': 'Std Name',
'contact_name': 'Std Name',
'user_id': self.user_sales_leads.id,
}, {
'email_from': 'test.lang@test.example.com',
'lang_id': self.lang_en.id,
'name': 'Test Suggestion (lang)',
'user_id': False,
}, {
'name': 'Test Suggestion (partner_id)',
'partner_id': self.contact_1.id,
'user_id': self.user_sales_leads.id,
}, {
'name': 'Test Suggestion (partner no email)',
'partner_id': partner_no_email.id
'name': 'Test Suggestion (partner no email)',
'partner_id': partner_no_email.id,
'user_id': self.user_sales_leads.id
}, {
'name': 'Test Suggestion (partner no email with cc email)',
'partner_id': partner_no_email.id,
'email_cc': 'test_cc@odoo.com'
'name': 'Test Suggestion (partner no email with cc email)',
'partner_id': partner_no_email.id,
'email_cc': 'test_cc@odoo.com',
'user_id': self.user_sales_leads.id
}
])
for lead, expected_suggested in zip(
lead_format + lead_multi + lead_from + lead_partner + lead_partner_no_email + lead_partner_no_email_with_cc,
for lead, expected_suggested in zip(leads, [
[
[(False, '"New Customer" <new.customer.format@test.example.com>', None, 'Customer Email')],
[(False, '"Multi Name" <new.customer.multi.1@test.example.com,new.customer.2@test.example.com>', None, 'Customer Email')],
[(False, '"Std Name" <new.customer.simple@test.example.com>', None, 'Customer Email')],
[(self.contact_1.id, '"Philip J Fry" <philip.j.fry@test.example.com>', self.contact_1.lang, 'Customer')],
[(partner_no_email.id, 'Test Partner', partner_no_email.lang, 'Customer')],
[
(False, 'test_cc@odoo.com', None, 'CC Email'),
(partner_no_email.id, 'Test Partner', partner_no_email.lang, 'Customer')
]
]
):
with self.subTest(lead=lead, email_from=lead.email_from):
res = lead._message_get_suggested_recipients()[lead.id]
# here contact_name is guessed based on formatted email
{
'name': 'New Customer',
'email': 'new.customer.format@test.example.com',
'partner_id': False,
'create_values': {
'company_name': 'Format Name',
'is_company': False,
'type': 'contact',
'user_id': self.user_sales_leads.id,
},
},
], [
# here no contact name, just a partner name -> use email and set company_name
{
'name': 'new.customer.multi.1@test.example.com, new.customer.2@test.example.com',
'email': 'new.customer.multi.1@test.example.com', # only first found normalized email is kept
'partner_id': False,
'create_values': {
'company_name': 'Multi Name',
'is_company': False,
'type': 'contact',
'user_id': self.user_sales_leads.id,
},
}, {
'name': '',
'email': 'new.customer.2@test.example.com', # second found creates another contact
'partner_id': False,
'create_values': {}, # not targeted as primary lead customer hence no values
},
], [
# here no contact name, a partner name, but there exists a company with that name -> company
{
'name': 'new.customer.with.parent@test.example.com',
'email': 'new.customer.with.parent@test.example.com',
'partner_id': False,
'create_values': {
'is_company': False,
'parent_id': company_partner.id,
'type': 'contact',
'user_id': self.user_sales_leads.id,
},
},
], [
# here contact name -> individual
{
'name': 'Std Name',
'email': 'new.customer.simple@test.example.com',
'partner_id': False,
'create_values': {
'is_company': False,
'type': 'contact',
'user_id': self.user_sales_leads.id,
},
},
], [
# here check lang is in create_values
{
'name': 'test.lang@test.example.com',
'email': 'test.lang@test.example.com',
'partner_id': False,
'create_values': {
'is_company': False,
'lang': 'en_US',
'type': 'contact',
},
},
], [
{
'partner_id': self.contact_1.id,
'name': 'Philip J Fry',
'email': 'philip.j.fry@test.example.com',
'create_values': {},
},
], [
{
'partner_id': partner_no_email.id,
'name': 'Test Partner',
'email': False,
'create_values': {},
},
], [
{
'partner_id': partner_no_email.id,
'email': False,
'name': 'Test Partner',
'create_values': {},
}, {
'name': '',
'email': 'test_cc@odoo.com',
'partner_id': False,
'create_values': {},
},
],
]):
with self.subTest(lead_name=lead.name, email_from=lead.email_from):
res = lead._message_get_suggested_recipients(no_create=True)
self.assertEqual(len(res), len(expected_suggested))
self.assertEqual(res, expected_suggested)
for received, expected in zip(res, expected_suggested):
self.assertDictEqual(received, expected)
@users('user_sales_manager')
def test_lead_message_get_suggested_recipients_values_for_create(self):
"""Check default creates value used when creating client from suggested
recipients (customer)."""
lead_details_for_contact = {
'street': '3rd Floor, Room 3-C',
'street2': '123 Arlington Avenue',
'zip': '13202',
'city': 'New York',
'country_id': self.env.ref('base.us').id,
'state_id': self.env.ref('base.state_us_39').id,
'website': 'https://www.arlington123.com/3f3c',
'phone': '678-728-0949',
'function': 'Delivery Boy',
'user_id': self.user_sales_manager.id,
}
for partner_name, contact_name, email in [
(False, 'ContactOnly', 'test_default_create@example.com'),
('Delivery Boy company', 'ContactAndCompany', 'default_create_with_partner@example.com'),
('Delivery Boy company', '', '"Contact Name" <default_create_with_name_in_email@example.com>'),
('Delivery Boy company', '', 'default_create_with_partner_no_name@example.com'),
('', '', 'lenny.bar@gmail.com'),
]:
if email == '"Contact Name" <default_create_with_name_in_email@example.com>':
suggested_email = 'default_create_with_name_in_email@example.com'
suggested_name = "Contact Name"
else:
suggested_email = email
# if no contact_name: fallback on the email to at least have something
suggested_name = contact_name or email
with self.subTest(partner_name=partner_name, contact_name=contact_name, email=email):
description = '<p>Top</p>'
lead1 = self.env['crm.lead'].create({
'name': 'TestLead',
'contact_name': contact_name,
'email_from': email,
'lang_id': self.lang_en.id,
'description': description,
'partner_name': partner_name,
**lead_details_for_contact,
})
suggestion = lead1._message_get_suggested_recipients(no_create=True)[0]
self.assertFalse(suggestion.get('partner_id'))
self.assertEqual(suggestion['email'], suggested_email)
self.assertEqual(suggestion['name'], suggested_name)
create_values = suggestion['create_values']
customer_information = lead1._get_customer_information().get(email_normalize(email), {})
customer_information.pop('name', False) # not keps in create_values, as already in name / email info
self.assertEqual(create_values, customer_information)
for field, value in lead_details_for_contact.items():
self.assertEqual(create_values.get(field), value)
self.assertEqual(create_values['comment'], description) # description -> comment
# parent company not created even if partner_name is set
self.assertFalse(create_values.get('parent_id')) # not supported, even if partner_name set
# company_name set only for contacts with partner_name (and no contact_name nor name in email)
if partner_name:
self.assertEqual(create_values['company_name'], partner_name) # partner_name -> company_name
else:
self.assertFalse('company_name' in create_values)
# it will normally never be a company, unless called despite a contact being already present (shouldn't happen)
self.assertEqual(create_values['is_company'], False)
def test_new_lead_notification(self):
""" Test newly create leads like from the website. People and channels
@ -104,15 +287,14 @@ class NewLeadNotification(TestCrmCommon):
@mute_logger('odoo.addons.mail.models.mail_thread')
def test_new_lead_from_email_multicompany(self):
company0 = self.env.company
company1 = self.env['res.company'].create({'name': 'new_company'})
company1 = self.company_2
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_team_model_id = self.env['ir.model']._get_id('crm.team')
crm_lead_model_id = self.env['ir.model']._get_id('crm.lead')
crm_team0 = self.env['crm.team'].create({
'name': 'crm team 0',
@ -124,16 +306,18 @@ class NewLeadNotification(TestCrmCommon):
})
mail_alias0 = self.env['mail.alias'].create({
'alias_domain_id': company0.alias_domain_id.id,
'alias_name': 'sale_team_0',
'alias_model_id': crm_lead_model.id,
'alias_parent_model_id': crm_team_model.id,
'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_domain_id': company1.alias_domain_id.id,
'alias_name': 'sale_team_1',
'alias_model_id': crm_lead_model.id,
'alias_parent_model_id': crm_team_model.id,
'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,
})
@ -141,12 +325,12 @@ class NewLeadNotification(TestCrmCommon):
crm_team0.write({'alias_id': mail_alias0.id})
crm_team1.write({'alias_id': mail_alias1.id})
new_message0 = """MIME-Version: 1.0
new_message0 = f"""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
To: {mail_alias0.display_name}
Content-Type: multipart/alternative; boundary="000000000000a47519057e029630"
--000000000000a47519057e029630
@ -162,12 +346,12 @@ Content-Transfer-Encoding: quoted-printable
--000000000000a47519057e029630--
"""
new_message1 = """MIME-Version: 1.0
new_message1 = f"""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
To: {mail_alias1.display_name}
Content-Type: multipart/alternative; boundary="000000000000a47519057e029630"
--000000000000a47519057e029630
@ -193,3 +377,46 @@ Content-Transfer-Encoding: quoted-printable
self.assertEqual(crm_lead0.company_id, company0)
self.assertEqual(crm_lead1.company_id, company1)
@users('user_sales_manager')
def test_incoming_email_automatic_lead_assignment(self):
# create a second team with a set leader
leader_team_2 = self.env['res.users'].sudo().create({'name': 'bob', 'login': 'bob'})
team_2 = self.env['crm.team'].create({
'name': 'team_2',
'alias_name': 'team.2',
'user_id': leader_team_2.id,
})
# send three emails to the aliases of both teams
for x in range(3):
self.format_and_process(
INCOMING_EMAIL,
f'source.email@customerOfTeam1{x}.be',
self.sales_team_1.alias_email,
subject=f'OpportunityTeam1{x}',
target_model='crm.lead',
)
self.format_and_process(
INCOMING_EMAIL,
f'source.email@customerOfTeam2{x}.be',
team_2.alias_email,
subject=f'OpportunityTeam2{x}',
target_model='crm.lead',
)
# each team should receive all three of their new opportunities and none of the others'
team1_leads = self.env['crm.lead'].search([
('team_id', '=', self.sales_team_1.id),
('email_from', 'ilike', 'source.email@customerOfTeam'),
])
team2_leads = self.env['crm.lead'].search([
('team_id', '=', team_2.id),
('email_from', 'ilike', 'source.email@customerOfTeam'),
])
for lead in team1_leads:
self.assertTrue('source.email@customerOfTeam1' in lead.email_from)
self.assertTrue(lead.user_id == self.user_sales_manager)
for lead in team2_leads:
self.assertTrue('source.email@customerOfTeam2' in lead.email_from)
self.assertTrue(lead.user_id == leader_team_2)

View file

@ -1,23 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import tools
from odoo import exceptions, tools
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.fields import Date
from odoo.tests import Form, tagged, users, loaded_demo_data
from odoo.tests import Form, tagged, users
from odoo.tests.common import TransactionCase
from odoo.tools import mute_logger
@tagged('crm_lead_pls')
class TestCRMPLS(TransactionCase):
class CrmPlsCommon(TransactionCase):
@classmethod
def setUpClass(cls):
""" Keep a limited setup to ensure tests are not impacted by other
records created in CRM common. """
super(TestCRMPLS, cls).setUpClass()
super().setUpClass()
cls.company_main = cls.env.user.company_id
cls.user_sales_manager = mail_new_test_user(
@ -32,26 +30,28 @@ class TestCRMPLS(TransactionCase):
'name': 'PLS Team',
})
# Ensure independance on demo data
# Ensure independence on demo data
cls.env['crm.lead'].with_context({'active_test': False}).search([]).unlink()
cls.env['crm.lead.scoring.frequency'].search([]).unlink()
cls.cr.flush()
def _get_lead_values(self, team_id, name_suffix, country_id, state_id, email_state, phone_state, source_id, stage_id):
def _prepare_test_lead_values(self, team_id, name_suffix, country_id, state_id, email_state, phone_state, source_id, stage_id):
return {
'name': 'lead_' + name_suffix,
'stage_id': stage_id,
'team_id': team_id,
'type': 'opportunity',
'state_id': state_id,
# contact
'email_state': email_state,
'phone_state': phone_state,
'source_id': source_id,
'stage_id': stage_id,
# address
'country_id': country_id,
'team_id': team_id
'state_id': state_id,
# misc
'source_id': source_id,
}
def generate_leads_with_tags(self, tag_ids):
Lead = self.env['crm.lead']
def _generate_leads_with_tags(self, tag_ids):
team_id = self.env['crm.team'].create({
'name': 'blup',
}).id
@ -77,15 +77,18 @@ class TestCRMPLS(TransactionCase):
'team_id': team_id
})
leads_with_tags = Lead.create(leads_to_create)
leads_with_tags = self.env['crm.lead'].create(leads_to_create)
return leads_with_tags
@tagged('post_install', '-at_install', 'crm_lead_pls')
class TestConfig(CrmPlsCommon):
def test_crm_lead_pls_update(self):
""" We test here that the wizard for updating probabilities from settings
is getting correct value from config params and after updating values
from the wizard, the config params are correctly updated
"""
""" Test the wizard for updating probabilities from settings is getting
correct value from config params and after updating values from the wizard
config params are correctly updated. """
# Set the PLS config
frequency_fields = self.env['crm.lead.scoring.frequency.field'].search([])
pls_fields_str = ','.join(frequency_fields.mapped('field_id.name'))
@ -115,6 +118,25 @@ class TestCRMPLS(TransactionCase):
self.assertEqual(IrConfigSudo.get_param("crm.pls_start_date"), date_to_update, 'Correct date is updated in config')
self.assertEqual(IrConfigSudo.get_param("crm.pls_fields"), fields_after_updation_str, 'Correct fields are updated in config')
def test_settings_pls_start_date(self):
""" Test various use cases of 'crm.pls_start_date' """
str_date_8_days_ago = Date.to_string(Date.today() - timedelta(days=8))
for value, expected in [
("2021-10-10", "2021-10-10"),
# empty of invalid value -> set to 8 days before today
("", str_date_8_days_ago),
("One does not simply walk into system parameters to corrupt them", str_date_8_days_ago),
]:
with self.subTest(value=value):
self.env['ir.config_parameter'].sudo().set_param('crm.pls_start_date', value)
res_config_new = self.env['res.config.settings'].new()
self.assertEqual(Date.to_string(res_config_new.predictive_lead_scoring_start_date), expected)
@tagged('post_install', '-at_install', 'crm_lead_pls')
class TestCrmPls(CrmPlsCommon):
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:
@ -141,32 +163,32 @@ class TestCRMPLS(TransactionCase):
# 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]))
self._prepare_test_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]))
self._prepare_test_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]))
self._prepare_test_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]))
self._prepare_test_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]))
self._prepare_test_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]))
self._prepare_test_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]))
self._prepare_test_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]))
self._prepare_test_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]))
# for leads with no team
leads_to_create.append(
self._get_lead_values(False, 'no_team_%s' % str(10), country_ids[1], state_ids[1], state_values[2], state_values[0], source_ids[1], stage_ids[2]))
self._prepare_test_lead_values(False, 'no_team_%s' % str(10), country_ids[1], state_ids[1], state_values[2], state_values[0], source_ids[1], stage_ids[2]))
leads_to_create.append(
self._get_lead_values(False, 'no_team_%s' % str(11), country_ids[0], state_ids[1], state_values[1], state_values[1], source_ids[0], stage_ids[0]))
self._prepare_test_lead_values(False, 'no_team_%s' % str(11), country_ids[0], state_ids[1], state_values[1], state_values[1], source_ids[0], stage_ids[0]))
leads_to_create.append(
self._get_lead_values(False, 'no_team_%s' % str(12), country_ids[1], state_ids[2], state_values[0], state_values[1], source_ids[2], stage_ids[0]))
self._prepare_test_lead_values(False, 'no_team_%s' % str(12), country_ids[1], state_ids[2], state_values[0], state_values[1], source_ids[2], stage_ids[0]))
leads_to_create.append(
self._get_lead_values(False, 'no_team_%s' % str(13), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[2], stage_ids[1]))
self._prepare_test_lead_values(False, 'no_team_%s' % str(13), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[2], stage_ids[1]))
leads = Lead.create(leads_to_create)
@ -283,7 +305,8 @@ class TestCRMPLS(TransactionCase):
self.assertEqual(leads[8].is_automated_probability, True)
# Restore -> Should decrease lost
leads[4].toggle_active()
leads[4].action_unarchive()
self.assertEqual(leads[4].won_status, 'pending')
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
@ -304,6 +327,7 @@ class TestCRMPLS(TransactionCase):
# set to won stage -> Should increase won
leads[4].stage_id = won_stage_id
self.assertEqual(leads[4].won_status, 'won')
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
@ -313,36 +337,52 @@ class TestCRMPLS(TransactionCase):
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
# Archive in won stage -> Should NOT decrease won NOR increase lost
# as lost = archived + 0% and WON = won_stage (+ 100%)
leads[4].action_archive()
self.assertEqual(leads[4].won_status, 'won')
self.assertEqual(lead_4_stage_0_freq.won_count, 2.1) # unchanged
self.assertEqual(lead_4_stage_won_freq.won_count, 2.1) # unchanged
self.assertEqual(lead_4_country_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_email_state_freq.won_count, 2.1) # unchanged
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
# Move to original stage -> Should do nothing (as lead is still lost)
# Move to original stage -> lead is not won anymore but not lost as probability != 0
leads[4].stage_id = stage_ids[0]
self.assertEqual(leads[4].won_status, 'pending')
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, 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
# force proba to 0% -> as already archived, will be lost (lost = archived AND 0%)
leads[4].probability = 0
self.assertEqual(leads[4].won_status, 'lost')
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
self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # +1
self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) # unchanged - should not increase lost frequency of won stage.
self.assertEqual(lead_4_country_freq.lost_count, 2.1) # +1
self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # +1
# Restore -> Should decrease lost - at the end, frequencies should be like first frequencyes tests (except for 0.0 -> 0.1)
leads[4].toggle_active()
leads[4].action_unarchive()
self.assertEqual(leads[4].won_status, 'pending')
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_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
@ -356,7 +396,7 @@ class TestCRMPLS(TransactionCase):
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_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
@ -398,7 +438,7 @@ class TestCRMPLS(TransactionCase):
{'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 = 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
@ -500,9 +540,9 @@ class TestCRMPLS(TransactionCase):
team_id = self.env['crm.team'].create({'name': 'Team Test 1'}).id
# create two leads
leads = Lead.create([
self._get_lead_values(team_id, 'edge pending', country_id, False, False, False, False, stage_id),
self._get_lead_values(team_id, 'edge lost', country_id, False, False, False, False, stage_id),
self._get_lead_values(team_id, 'edge won', country_id, False, False, False, False, stage_id),
self._prepare_test_lead_values(team_id, 'edge pending', country_id, False, False, False, False, stage_id),
self._prepare_test_lead_values(team_id, 'edge lost', country_id, False, False, False, False, stage_id),
self._prepare_test_lead_values(team_id, 'edge won', country_id, False, False, False, False, stage_id),
])
# set a new tag
leads.tag_ids = self.env['crm.tag'].create({'name': 'lead scoring edge case'})
@ -540,27 +580,6 @@ class TestCRMPLS(TransactionCase):
self.assertEqual(tools.float_compare(leads[1].probability, 0, 2), 0)
self.assertEqual(tools.float_compare(leads[0].probability, 0.01, 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 = Date.to_string(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(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(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(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")
def test_pls_no_share_stage(self):
""" We test here the situation where all stages are team specific, as there is
a current limitation (can be seen in _pls_get_won_lost_total_count) regarding
@ -568,47 +587,254 @@ class TestCRMPLS(TransactionCase):
to have no team assigned to it."""
Lead = self.env['crm.lead']
team_id = self.env['crm.team'].create([{'name': 'Team Test'}]).id
self.env['crm.stage'].search([('team_id', '=', False)]).write({'team_id': team_id})
self.env['crm.stage'].search([('team_ids', '=', False)]).write({'team_ids': [team_id]})
lead = Lead.create({'name': 'team', 'team_id': team_id, 'probability': 41.23})
Lead._cron_update_automated_probabilities()
self.assertEqual(tools.float_compare(lead.probability, 41.23, 2), 0)
self.assertEqual(tools.float_compare(lead.automated_probability, 0, 2), 0)
def test_pls_tooltip_data(self):
""" Assert that the method preparing tooltip data correctly returns (field, couple)
values, in order of importance, of TOP 3 and LOW 3 criterions in PLS computation.
See Table in docstring below for more details and a practical situation."""
Lead = self.env['crm.lead']
self.env['ir.config_parameter'].sudo().set_param(
"crm.pls_fields",
"country_id,state_id,email_state,phone_state,source_id"
)
country_ids = self.env['res.country'].search([], limit=2).ids
source_ids = self.env['utm.source'].search([], limit=2).ids
stage_ids = self.env['crm.stage'].search([], limit=3).ids
state_ids = self.env['res.country.state'].search([], limit=2).ids
team_id = self.env['crm.team'].create([{'name': 'Team Tooltip'}]).id
leads = Lead.create([
self._prepare_test_lead_values(team_id, 'lead Won A', country_ids[0], state_ids[0], False, False, source_ids[1], stage_ids[0]),
self._prepare_test_lead_values(team_id, 'lead Won B', country_ids[1], state_ids[0], False, False, False, stage_ids[0]),
self._prepare_test_lead_values(team_id, 'lead Lost C', False, False, False, False, source_ids[0], stage_ids[0]),
self._prepare_test_lead_values(team_id, 'lead Lost D', country_ids[0], False, False, False, source_ids[0], stage_ids[0]),
self._prepare_test_lead_values(team_id, 'lead Lost E', False, state_ids[1], False, False, False, stage_ids[2]),
self._prepare_test_lead_values(team_id, 'lead Tooltip', country_ids[0], state_ids[0], False, False, source_ids[0], stage_ids[1]),
])
# On creation, as phone and email are not set, these two fields will be set to False
leads.email_state = 'correct'
(leads[0] | leads[1] | leads[4] | leads[5]).phone_state = 'correct'
(leads[2] | leads[3]).phone_state = 'incorrect'
leads[:2].action_set_won()
leads[2:5].action_set_lost()
Lead._cron_update_automated_probabilities()
self.env.invalidate_all()
# Values for leads[5]:
# pW / pL is the probability that a won / lost lead has the lead value for a given field
# [Score = pW / (pW + pL)] -> A score above .5 is a TOP, below .5 a LOW, equal to .5 ignored
# Exception : for stage_id -> Score = 1 - P(current stage or lower for a lost lead)
# ------------------------------------------------------------------------------------------
# -- LOW 3 (lowest first, only 2 here)
# source_id: pW = 0.1/1.2 pL = 2.1/2.2 -> Score = 0.08
# country_id: pW = 1.1/2.2 pL = 1.1/1.2 -> Score = 0.353
# -- Neither
# email_state: pW = 2.1/2.1 pL = 3.1/3.1 -> Score = 0.5
# -- TOP 3 (highest first)
# state_id: pW = 2.1/2.2 pL = 0.1/1.2 -> Score = 0.92
# phone_state: pW = 2.1/2.2 pL = 1.1/3.2 -> Score = 0.735
# stage_id: pL = 1.1/3.1 -> Score = 0.645
expected_low_3 = ['source_id', 'country_id']
expected_top_3 = ['state_id', 'phone_state', 'stage_id']
tooltip_data = leads[5].prepare_pls_tooltip_data()
self.assertEqual('Team Tooltip', tooltip_data['team_name'])
self.assertEqual(tools.float_compare(tooltip_data['probability'], 74.30, 2), 0)
self.assertListEqual([top_entry.get('field') for top_entry in tooltip_data['top_3_data']], expected_top_3)
self.assertListEqual([low_entry.get('field') for low_entry in tooltip_data['low_3_data']], expected_low_3)
# Assert scores for phone/email_state are excluded if absurd,
# e.g. in top 3 when incorrect / not set or in low 3 if correct
# Stage does not change and always has a score of 0.645
self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", "email_state,phone_state")
leads[5].phone_state = False
leads[5].email_state = 'incorrect'
leads[:2].phone_state = False
leads[:2].email_state = 'incorrect'
leads[2:5].phone_state = 'correct'
leads[2:5].email_state = 'correct'
Lead._cron_update_automated_probabilities()
self.env.invalidate_all()
# phone_state: pW = 2.1/2.2 pL = 0.1/3.2 -> Score = 0.968
# email_state: pW = 2.1/2.2 pL = 0.1/3.2 -> Score = 0.968
tooltip_data = leads[5].prepare_pls_tooltip_data()
self.assertEqual(['stage_id'], [entry['field'] for entry in tooltip_data['top_3_data']])
self.assertFalse(tooltip_data['low_3_data'])
leads[5].email_state = 'correct'
leads[5].phone_state = 'incorrect'
leads[:2].phone_state = 'incorrect'
Lead._cron_update_automated_probabilities()
self.env.invalidate_all()
# phone_state: pW = 2.1/2.2 pL = 0.1/3.2 -> Score = 0.968
# email_state: pW = 0.1/2.2 pL = 3.1/3.2 -> Score = 0.045
tooltip_data = leads[5].prepare_pls_tooltip_data()
self.assertEqual(['stage_id'], [entry['field'] for entry in tooltip_data['top_3_data']])
self.assertFalse(tooltip_data['low_3_data'])
@tagged('post_install', '-at_install', 'crm_lead_pls')
class TestCrmPlsSides(CrmPlsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.team = cls.env['crm.team'].create([{'name': 'Team Test'}])
cls.stage_new, cls.stage_in_progress, cls.stage_won = cls.env['crm.stage'].create([
{
'name': 'New Stage',
'sequence': 1,
'team_ids': [cls.team.id],
}, {
'name': 'In Progress Stage',
'sequence': 2,
'team_ids': [cls.team.id],
}, {
'is_won': True,
'name': 'Won Stage',
'sequence': 3,
'team_ids': [cls.team.id],
},
])
@users('user_sales_manager')
def test_stage_update(self):
""" Test side effects of changing stages """
team_id = self.team.with_user(self.env.user).id
stage_new, _stage_in_progress, stage_won = (self.stage_new + self.stage_in_progress + self.stage_won).with_user(self.env.user)
leads = self.env['crm.lead'].create([
{
'name': 'Test Lead 1',
'probability': 50,
'stage_id': stage_new.id,
'team_id': team_id,
}, {
'name': 'Test Lead 2',
'probability': 50,
'stage_id': stage_new.id,
'team_id': team_id,
}
])
leads.action_set_lost()
for lead in leads:
self.assertFalse(lead.active)
self.assertFalse(lead.probability)
leads[0].active = True
# putting in won state should reactivate
leads.write({'stage_id': stage_won.id})
for lead in leads:
self.assertTrue(lead.active)
self.assertEqual(lead.probability, 100)
@users('user_sales_manager')
def test_won_lost_validity(self):
team_id = self.team.with_user(self.env.user).id
stage_new, stage_in_progress, stage_won = (self.stage_new + self.stage_in_progress + self.stage_won).with_user(self.env.user)
lead = self.env['crm.lead'].create([
{
'name': 'Test Lead',
'probability': 50,
'stage_id': stage_new.id,
'team_id': team_id,
}
])
self.assertEqual(lead.won_status, 'pending')
# Probability 100 is not a sufficient condition to win the lead
lead.write({'probability': 100})
self.assertEqual(lead.won_status, 'pending')
# Test won validity
lead.write({'probability': 90})
self.assertEqual(lead.won_status, 'pending')
lead.action_set_won()
self.assertEqual(lead.probability, 100)
self.assertTrue(lead.stage_id.is_won)
self.assertEqual(lead.won_status, 'won')
with self.assertRaises(exceptions.ValidationError, msg='A won lead cannot be set as lost.'):
lead.action_set_lost()
# Won lead can be inactive
lead.write({'active': False})
self.assertEqual(lead.probability, 100)
self.assertEqual(lead.won_status, 'won')
with self.assertRaises(exceptions.ValidationError, msg='A won lead cannot have probability < 100'):
lead.write({'probability': 75})
# Restore the lead in a non won stage. won_count = lost_count = 0.1 in frequency table. P = 50%
lead.write({'stage_id': stage_in_progress.id, 'active': True})
self.assertFalse(lead.probability == 100)
self.assertEqual(lead.won_status, 'pending')
# Test lost validity
lead.action_set_lost()
self.assertFalse(lead.active)
self.assertEqual(lead.probability, 0)
self.assertEqual(lead.won_status, 'lost')
# Test won validity reaching won stage
lead.write({'stage_id': stage_won.id})
self.assertTrue(lead.active)
self.assertEqual(lead.probability, 100)
self.assertEqual(lead.won_status, 'won')
# Back to lost
lead.write({'active': False, 'probability': 0, 'stage_id': stage_new.id})
self.assertEqual(lead.won_status, 'lost')
# Once active again, lead is not lost anymore
lead.write({'active': True})
self.assertEqual(lead.won_status, 'pending', "An active lead cannot be lost")
@users('user_sales_manager')
def test_team_unlink(self):
""" Test that frequencies are sent to "no team" when unlinking a team
in order to avoid losing too much informations. """
pls_team = self.env["crm.team"].browse(self.pls_team.ids)
# clean existing data
self.env["crm.lead.scoring.frequency"].sudo().search([('team_id', '=', False)]).unlink()
# existing no-team data
no_team = [
noteam_scoring_data = [
('stage_id', '1', 20, 10),
('stage_id', '2', 0.1, 0.1),
('stage_id', '3', 10, 0),
('country_id', '1', 10, 0.1),
]
self.env["crm.lead.scoring.frequency"].sudo().create([
{'variable': variable, 'value': value,
'won_count': won_count, 'lost_count': lost_count,
'team_id': False,
} for variable, value, won_count, lost_count in no_team
{
'lost_count': lost_count,
'team_id': False,
'value': value,
'variable': variable,
'won_count': won_count,
} for variable, value, won_count, lost_count in noteam_scoring_data
])
# add some frequencies to team to unlink
team = [
team_scoring_data = [
('stage_id', '1', 20, 10), # existing noteam
('country_id', '1', 0.1, 10), # existing noteam
('country_id', '2', 0.1, 0), # new but void
('country_id', '3', 30, 30), # new
]
existing_plsteam = self.env["crm.lead.scoring.frequency"].sudo().create([
{'variable': variable, 'value': value,
'won_count': won_count, 'lost_count': lost_count,
'team_id': pls_team.id,
} for variable, value, won_count, lost_count in team
{
'lost_count': lost_count,
'team_id': pls_team.id,
'value': value,
'variable': variable,
'won_count': won_count,
} for variable, value, won_count, lost_count in team_scoring_data
])
pls_team.unlink()
@ -633,3 +859,147 @@ class TestCRMPLS(TransactionCase):
self.assertEqual(frequency.won_count, stat[2])
self.assertEqual(frequency.lost_count, stat[3])
self.assertEqual(len(existing_noteam), len(final_noteam))
@tagged('lead_manage', 'crm_lead_pls')
class TestLeadLost(TestCrmCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.lost_reason = cls.env['crm.lost.reason'].create({
'name': 'Test Reason'
})
@users('user_sales_salesman')
def test_lead_lost(self):
""" Test setting a lead as lost using the wizard. Also check that an
'html editor' void content used as feedback is not logged on the lead. """
# Initial data
self.assertEqual(len(self.lead_1.message_ids), 1, 'Should contain creation message')
creation_message = self.lead_1.message_ids[0]
self.assertEqual(creation_message.subtype_id, self.env.ref('crm.mt_lead_create'))
self.assertEqual(
self.lead_1.message_partner_ids, self.user_sales_leads.partner_id,
'Responsible should be follower')
# Update responsible as ACLs is "own only" for user_sales_salesman
with self.mock_mail_gateway():
self.lead_1.with_user(self.user_sales_manager).write({
'user_id': self.user_sales_salesman.id,
'probability': 32,
})
self.flush_tracking()
lead = self.env['crm.lead'].browse(self.lead_1.ids)
self.assertFalse(lead.lost_reason_id)
self.assertEqual(
self.lead_1.message_partner_ids, self.user_sales_leads.partner_id + self.user_sales_salesman.partner_id,
'New responsible should be follower')
self.assertEqual(lead.probability, 32)
# tracking message
self.assertEqual(len(lead.message_ids), 2, 'Should have tracked new responsible')
update_message = lead.message_ids[0]
self.assertMessageFields(
update_message,
{
'notified_partner_ids': self.env['res.partner'],
'partner_ids': self.env['res.partner'],
'subtype_id': self.env.ref('mail.mt_note'),
'tracking_field_names': ['user_id'],
}
)
# mark as lost using the wizard
lost_wizard = self.env['crm.lead.lost'].create({
'lead_ids': lead.ids,
'lost_reason_id': self.lost_reason.id,
'lost_feedback': '<p></p>', # void content
})
lost_wizard.action_lost_reason_apply()
self.flush_tracking()
# check lead update
self.assertFalse(lead.active)
self.assertEqual(lead.automated_probability, 0)
self.assertEqual(lead.lost_reason_id, self.lost_reason) # TDE FIXME: should be called lost_reason_id non didjou
self.assertEqual(lead.probability, 0)
# check messages
self.assertEqual(len(lead.message_ids), 3, 'Should have logged a tracking message for lost lead with reason')
lost_message = lead.message_ids[0]
self.assertMessageFields(
lost_message,
{
'notified_partner_ids': self.env['res.partner'],
'partner_ids': self.env['res.partner'],
'subtype_id': self.env.ref('crm.mt_lead_lost'),
'tracking_field_names': ['active', 'lost_reason_id', 'won_status'],
'tracking_values': [
('active', 'boolean', True, False),
('lost_reason_id', 'many2one', False, self.lost_reason),
('won_status', 'char', 'Pending', 'Lost'),
],
}
)
@users('user_sales_leads')
def test_lead_lost_batch_wfeedback(self):
""" Test setting leads as lost in batch using the wizard, including a log
message. """
leads = self._create_leads_batch(lead_type='lead', count=10, probabilities=[10, 20, 30])
self.assertEqual(len(leads), 10)
self.flush_tracking()
lost_wizard = self.env['crm.lead.lost'].create({
'lead_ids': leads.ids,
'lost_reason_id': self.lost_reason.id,
'lost_feedback': '<p>I cannot find it. It was in my closet and pouf, disappeared.</p>',
})
lost_wizard.action_lost_reason_apply()
self.flush_tracking()
for lead in leads:
# check content
self.assertFalse(lead.active)
self.assertEqual(lead.automated_probability, 0)
self.assertEqual(lead.probability, 0)
self.assertEqual(lead.lost_reason_id, self.lost_reason)
# check messages
self.assertEqual(len(lead.message_ids), 2, 'Should have 2 messages: creation, lost with log')
lost_message = lead.message_ids.filtered(lambda msg: msg.subtype_id == self.env.ref('crm.mt_lead_lost'))
self.assertTrue(lost_message)
self.assertTracking(
lost_message,
[('active', 'boolean', True, False),
('lost_reason_id', 'many2one', False, self.lost_reason)
]
)
self.assertIn('<p>I cannot find it. It was in my closet and pouf, disappeared.</p>', lost_message.body,
'Feedback should be included directly within tracking message')
@users('user_sales_salesman')
@mute_logger('odoo.addons.base.models')
def test_lead_lost_crm_rights(self):
""" Test ACLs of lost reasons management and usage """
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(exceptions.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'
})
# nice try little salesman, you cannot invoke a wizard to update other people leads
with self.assertRaises(exceptions.AccessError):
# wizard needs to be here due to cache clearing in assertRaises
# (ORM does not load m2m records unavailable to the user from database)
lost_wizard = self.env['crm.lead.lost'].create({
'lead_ids': lead.ids,
'lost_reason_id': lost_reason.id
})
lost_wizard.action_lost_reason_apply()

View file

@ -0,0 +1,469 @@
from datetime import datetime
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.tests import tagged, users
@tagged('lead_internals')
class TestCrmLeadRainbowmanMessages(TestCrmCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# unlink all leads from sales_team_1
cls.env['crm.lead'].search([
('team_id', '=', cls.sales_team_1.id),
]).unlink()
cls.company_casey = cls.env['res.company'].create({
'name': 'company_casey',
})
cls.sales_manager_casey = mail_new_test_user(
cls.env,
login='sales_manager_casey',
name='sales_manager_casey',
groups='sales_team.group_sale_manager,base.group_partner_manager',
company_id=cls.company_casey.id,
company_ids=[(4, cls.company_casey.id)],
)
# cls.env['crm.team.member'].create([
# {'user_id': cls.user_sales_manager.id, 'crm_team_id': cls.sales_team_1.id},
# {'user_id': cls.user_sales_salesman.id, 'crm_team_id': cls.sales_team_1.id},
# ])
def _update_create_date(self, lead, date):
self.env.cr.execute("""
UPDATE crm_lead
SET create_date = %(date)s
WHERE id = %(lead_id)s
""", {
'lead_id': lead.id,
'date': date,
})
lead.invalidate_recordset(['create_date'])
def _set_won_get_rainbowman_message(self, lead, user, reset_team=False):
"""
Assign the passed user and set the lead as won.
Then, if there's a message, return that message.
Otherwise, as the result for action_set_won_rainbowman() if there's no message is True,
return False to make testing code more readable.
"""
# lead.user_id = user
# # If reset_team is passed, reset the team to False, as otherwise assigning a user will automatically assign a team
# if reset_team:
# lead.team_id = False
lead.update({
'user_id': user.id,
'team_id': False if reset_team else lead.team_id.id,
})
rainbowman_action_result = lead.with_user(user).action_set_won_rainbowman()
if rainbowman_action_result and not isinstance(rainbowman_action_result, bool):
return rainbowman_action_result['effect']['message']
return False
@users('user_sales_manager')
def test_leads_rainbowman(self):
"""
This test ensures that all rainbowman messages can trigger, and that they do so in correct order of priority.
"""
# setup timestamps:
past = datetime(2024, 12, 15, 12, 0)
jan1_10am = datetime(2025, 1, 1, 10, 0)
jan1_12pm = datetime(2025, 1, 1, 12, 0)
jan2 = datetime(2025, 1, 2, 12, 0)
jan3_12pm = datetime(2025, 1, 3, 12, 0)
jan3_1pm = datetime(2025, 1, 3, 13, 0)
jan4 = datetime(2025, 1, 4, 12, 0)
jan12 = datetime(2025, 1, 12, 12, 0)
march1 = datetime(2025, 3, 1, 12, 0)
# setup main batch of leads
with self.mock_datetime_and_now(past):
leads_norevenue = self._create_leads_batch(
count=15,
partner_count=5,
user_ids=[self.user_sales_manager.id, self.user_sales_salesman.id],
lead_type='opportunity',
additional_lead_values={
'stage_id': self.stage_team1_1.id,
},
)
leads_revenue = self._create_leads_batch(
count=18,
partner_count=3,
user_ids=[self.user_sales_manager.id, self.user_sales_salesman.id],
lead_type='opportunity',
additional_lead_values={
'expected_revenue': 500,
'stage_id': self.stage_team1_1.id,
},
)
iter_leads_norevenue = iter(leads_norevenue)
iter_leads_revenue = iter(leads_revenue)
all_leads = leads_norevenue | leads_revenue
# initialize tracking
self.flush_tracking()
# test lead rainbowman messages (leads without expected revenues)
with self.mock_datetime_and_now(jan1_10am):
self.flush_tracking()
all_leads.invalidate_recordset(['duration_tracking'])
# switch the stage to avoid having the "first to last stage" message show up all the time
all_leads.write({'stage_id': self.stage_team1_2.id})
# flush tracking to make sure it's taken into account
self.flush_tracking()
all_leads.invalidate_recordset(['duration_tracking'])
msg_firstdeal = self._set_won_get_rainbowman_message(next(iter_leads_norevenue), self.user_sales_manager)
self.assertEqual(
msg_firstdeal,
'Go, go, go! Congrats for your first deal.',
'First deal',
)
lead_25messages = next(iter_leads_norevenue)
self.env['mail.message'].create([
{
'model': 'crm.lead',
'res_id': lead_25messages.id,
'body': 'Message',
'message_type': 'comment',
} for x in range(25)
])
msg_25messages = self._set_won_get_rainbowman_message(lead_25messages, self.user_sales_manager)
self.assertEqual(
msg_25messages,
'Phew, that took some effort — but you nailed it. Good job!',
'Win with 25 messages on the counter',
)
with self.mock_datetime_and_now(jan1_12pm):
self.flush_tracking()
all_leads.invalidate_recordset(['duration_tracking'])
lead_other_first_with_revenue = next(iter_leads_norevenue)
lead_other_first_with_revenue.expected_revenue = 100
msg_other_first_with_revenue = self._set_won_get_rainbowman_message(lead_other_first_with_revenue, self.user_sales_salesman)
self.assertEqual(
msg_other_first_with_revenue,
'Go, go, go! Congrats for your first deal.',
'First deal (another user), even with record revenue',
)
lead_first_country = next(iter_leads_norevenue)
lead_first_country.country_id = self.env.ref('base.au')
msg_first_country = self._set_won_get_rainbowman_message(lead_first_country, self.user_sales_manager)
self.assertEqual(
msg_first_country,
'You just expanded the map! First win in Australia.',
'First win in a country (all team)',
)
lead_second_country = next(iter_leads_norevenue)
lead_second_country.country_id = self.env.ref('base.au')
msg_second_country = self._set_won_get_rainbowman_message(lead_second_country, self.user_sales_salesman)
self.assertFalse(
msg_second_country,
'Second deal from the same country (all team)',
)
source_facebook_ad = self.env['utm.source'].create({'name': 'Facebook Ad'})
lead_first_source = next(iter_leads_norevenue)
lead_first_source.source_id = source_facebook_ad
msg_first_source = self._set_won_get_rainbowman_message(lead_first_source, self.user_sales_manager)
self.assertEqual(
msg_first_source,
'Yay, your first win from Facebook Ad!',
'First win from a UTM source (all team)',
)
lead_second_source = next(iter_leads_norevenue)
lead_second_source.source_id = source_facebook_ad.id
msg_second_source = self._set_won_get_rainbowman_message(lead_second_source, self.user_sales_salesman)
self.assertFalse(
msg_second_source,
'Second deal from the same source (all team)',
)
lead_combo5 = next(iter_leads_norevenue)
msg_combo5 = self._set_won_get_rainbowman_message(lead_combo5, self.user_sales_manager)
self.assertEqual(
msg_combo5,
'You\'re on fire! Fifth deal won today 🔥',
'Fifth deal won today (user)',
)
with self.mock_datetime_and_now(jan2):
self.flush_tracking()
all_leads.invalidate_recordset(['duration_tracking'])
# fast closes:
# 10 days ago
lead_fastclose_10 = next(iter_leads_norevenue)
self._update_create_date(lead_fastclose_10, datetime(2024, 12, 22))
msg_fastclose_10 = self._set_won_get_rainbowman_message(lead_fastclose_10, self.user_sales_manager)
self.assertEqual(
msg_fastclose_10,
'Wow, that was fast. That deal didnt stand a chance!',
'Fastest close in 30 days',
)
# 15 days ago
lead_fastclose_15 = next(iter_leads_norevenue)
self._update_create_date(lead_fastclose_15, datetime(2024, 12, 17))
msg_fastclose_15 = self._set_won_get_rainbowman_message(lead_fastclose_15, self.user_sales_manager)
self.assertFalse(
msg_fastclose_15,
'Not the fastest close in 30 days',
)
# Today
lead_fastclose_0 = next(iter_leads_norevenue)
self._update_create_date(lead_fastclose_0, jan1_12pm)
msg_fastclose_0 = self._set_won_get_rainbowman_message(lead_fastclose_0, self.user_sales_manager)
self.assertEqual(
msg_fastclose_0,
'Wow, that was fast. That deal didnt stand a chance!',
'Fastest close in 30 days',
)
self.assertFalse(
self._set_won_get_rainbowman_message(next(iter_leads_norevenue), self.user_sales_salesman),
'No achievment reached',
)
with self.mock_datetime_and_now(jan3_12pm):
self.flush_tracking()
all_leads.invalidate_recordset(['duration_tracking'])
lead_3daystreak = next(iter_leads_norevenue)
msg_3daystreak = self._set_won_get_rainbowman_message(lead_3daystreak, self.user_sales_manager)
self.assertEqual(
msg_3daystreak,
'You\'re on a winning streak. 3 deals in 3 days, congrats!',
'Three-day streak',
)
with self.mock_datetime_and_now(jan3_1pm):
self.flush_tracking()
all_leads.invalidate_recordset(['duration_tracking'])
# Create new lead with no changed stage to get 'straight to the win' message
lead_first_to_last = self.env['crm.lead'].create({
'name': 'lead',
'type': 'opportunity',
'stage_id': self.stage_team1_1.id,
'user_id': self.user_sales_manager.id,
})
self._update_create_date(lead_first_to_last, jan1_12pm)
self.flush_tracking()
all_leads.invalidate_recordset(['duration_tracking'])
msg_first_to_last = self._set_won_get_rainbowman_message(lead_first_to_last, self.user_sales_manager)
self.assertEqual(
msg_first_to_last,
'No detours, no delays - from New straight to the win! 🚀',
'First stage to last stage',
)
self.assertFalse(
self._set_won_get_rainbowman_message(next(iter_leads_norevenue), self.user_sales_manager),
'Check that no message is returned if no "achievement" is reached',
)
with self.mock_datetime_and_now(jan4):
# test lead rainbowman messages (leads with expected revenues)
last_30_days_cases = [
(self.user_sales_manager, 650, 'Boom! Team record for the past 30 days.'),
(self.user_sales_manager, 550, False),
(self.user_sales_manager, 700, 'Boom! Team record for the past 30 days.'),
(self.user_sales_manager, 700, False),
(self.user_sales_salesman, 600, 'You just beat your personal record for the past 30 days.'),
(self.user_sales_salesman, 600, False),
(self.user_sales_salesman, 550, False),
(self.user_sales_salesman, 1000, 'Boom! Team record for the past 30 days.'),
(self.user_sales_manager, 950, 'You just beat your personal record for the past 30 days.'),
]
for user, expected_revenue, expected_message in last_30_days_cases:
with self.subTest(user=user, revenue=expected_revenue):
lead_revenue = next(iter_leads_revenue)
lead_revenue.expected_revenue = expected_revenue
msg_revenue = self._set_won_get_rainbowman_message(lead_revenue, user)
self.assertEqual(msg_revenue, expected_message)
with self.mock_datetime_and_now(jan12):
last_7_days_cases = [
(self.user_sales_manager, 650, 'Yeah! Best deal out of the last 7 days for the team.'),
(self.user_sales_manager, 500, False),
(self.user_sales_manager, 650, False),
(self.user_sales_manager, 800, 'Yeah! Best deal out of the last 7 days for the team.'),
(self.user_sales_salesman, 700, 'You just beat your personal record for the past 7 days.'),
(self.user_sales_salesman, 650, False),
(self.user_sales_salesman, 750, 'You just beat your personal record for the past 7 days.'),
(self.user_sales_salesman, 850, 'Yeah! Best deal out of the last 7 days for the team.'),
]
for user, expected_revenue, expected_message in last_7_days_cases:
with self.subTest(user=user, revenue=expected_revenue):
lead_revenue = next(iter_leads_revenue)
lead_revenue.expected_revenue = expected_revenue
msg_revenue = self._set_won_get_rainbowman_message(lead_revenue, user)
self.assertEqual(msg_revenue, expected_message)
with self.mock_datetime_and_now(march1):
lead_later_record = next(iter_leads_revenue)
lead_later_record.expected_revenue = 750
msg_later_record = self._set_won_get_rainbowman_message(lead_later_record, self.user_sales_manager)
self.assertEqual(msg_later_record, 'Boom! Team record for the past 30 days.', 'Once a month has passed, \
monthly team records may be set even if the amount was lower than the alltime max.')
# cross-year case
current_dt = datetime(2026, 1, 5, 12, 0)
past_dt = datetime(2025, 12, 1, 12, 0)
with self.mock_datetime_and_now(past_dt):
lead_cross_year = self.env['crm.lead'].create({
'name': 'lead_future_create',
'type': 'opportunity',
'stage_id': self.stage_team1_won.id,
'user_id': self.user_sales_manager.id,
'expected_revenue': 500.0,
})
with self.mock_datetime_and_now(current_dt):
msg = self._set_won_get_rainbowman_message(lead_cross_year, self.sales_manager_casey)
self.assertFalse(msg)
@users('user_sales_manager')
def test_leads_rainbowman_timezones(self):
"""
Users in differing timezones need to get appropriate time-based messages.
This test verifies that users in distant timezones still get rainbowman messages
when it makes sense from their own point of view.
"""
sales_m10 = mail_new_test_user( # UTC-10
self.env(su=True),
login='polynesia_-10',
tz='Pacific/Honolulu',
name='polynesia_-10',
groups='sales_team.group_sale_manager',
)
sales_p530 = mail_new_test_user( # UTC+5:30
self.env(su=True),
login='india_+5:30',
tz='Asia/Kolkata',
name='india_+5:30',
groups='sales_team.group_sale_manager',
)
sales_p13 = mail_new_test_user( # UTC+13
self.env(su=True),
login='samoa_+13',
tz='Pacific/Apia',
name='samoa_+13',
groups='sales_team.group_sale_manager',
)
sales_users = [sales_m10, sales_p530, sales_p13]
# All datetimes stored in-DB are in UTC
jan9_10_45am = datetime(2025, 1, 9, 10, 45, 0) # first deal
jan9_11_30am = datetime(2025, 1, 9, 11, 30, 0)
jan9_4pm = datetime(2025, 1, 9, 16, 0)
jan9_6_45pm = datetime(2025, 1, 9, 18, 45)
jan9_11pm = datetime(2025, 1, 9, 23, 0) # polynesia_m10: fifth deal in a day
jan10_midnight = datetime(2025, 1, 10, 0, 0) # samoa_p13: fifth deal in a day
jan10_3am = datetime(2025, 1, 10, 3, 0)
jan10_8am = datetime(2025, 1, 10, 8, 0) # india_p530: fifth deal in a day
jan10_11am = datetime(2025, 1, 10, 11, 0) # samoa_p13: three-day streak
first_deal = 'Go, go, go! Congrats for your first deal.'
fifth_deal_day = 'You\'re on fire! Fifth deal won today 🔥'
three_day_streak = 'You\'re on a winning streak. 3 deals in 3 days, congrats!'
cases = [
(jan9_10_45am, {user: first_deal for user in sales_users}),
(jan9_11_30am, {}),
(jan9_4pm, {}),
(jan9_6_45pm, {}),
(jan9_11pm, {sales_m10: fifth_deal_day}),
(jan10_midnight, {sales_p13: fifth_deal_day}),
(jan10_3am, {}),
(jan10_8am, {sales_p530: fifth_deal_day}),
(jan10_11am, {sales_p13: three_day_streak}),
]
leads = self._create_leads_batch(
count=27,
lead_type='opportunity',
additional_lead_values={
'stage_id': self.stage_team1_1.id,
},
)
iter_leads = iter(leads)
for deal_closing_time, expected_messages in cases:
with self.mock_datetime_and_now(deal_closing_time):
for sales_user in sales_users:
with self.subTest(username=sales_user.name, time=deal_closing_time):
msg = self._set_won_get_rainbowman_message(next(iter_leads), sales_user)
self.assertEqual(msg, expected_messages.get(sales_user, False))
@users('sales_manager_casey')
def test_leads_rainbowman_no_team(self):
past = datetime(2025, 1, 2, 12, 0)
past_1pm = datetime(2025, 1, 2, 13, 0)
now = datetime(2025, 1, 5, 12, 0)
with self.mock_datetime_and_now(past):
leads = self._create_leads_batch(
count=6,
user_ids=[self.sales_manager_casey.id, self.user_sales_salesman.id],
lead_type='opportunity',
additional_lead_values={
'stage_id': self.stage_team1_1.id,
},
)
iter_leads = iter(leads)
with self.mock_datetime_and_now(past_1pm):
# prime the users and leads (to skip first deal closed, fastest close, from first to last...)
self.flush_tracking()
leads.stage_id = self.stage_gen_1
self.flush_tracking()
lead_prime_casey = next(iter_leads)
lead_prime_benoit = next(iter_leads)
self._set_won_get_rainbowman_message(lead_prime_casey, self.sales_manager_casey, reset_team=True)
self._set_won_get_rainbowman_message(lead_prime_benoit, self.user_sales_salesman)
with self.mock_datetime_and_now(now):
source_xitter_post = self.env['utm.source'].create({'name': 'Xitter Post'})
lead_noteam = next(iter_leads)
lead_noteam.source_id = source_xitter_post
msg_lead_noteam = self._set_won_get_rainbowman_message(lead_noteam, self.sales_manager_casey, reset_team=True)
self.assertEqual(
msg_lead_noteam,
'Yay, your first win from Xitter Post!',
'First win from a UTM source (lead has no team)',
)
# (complete an empty lead to skip the fifth row in a day message)
self._set_won_get_rainbowman_message(next(iter_leads), self.sales_manager_casey)
lead_noteam_samesource = next(iter_leads)
lead_noteam_samesource.source_id = source_xitter_post
msg_lead_noteam_samesource = self._set_won_get_rainbowman_message(lead_noteam_samesource, self.sales_manager_casey, reset_team=True)
self.assertFalse(
msg_lead_noteam_samesource,
'Second deal from the same source (no team) triggers no message if the source has already been won once for the user',
)
lead_inteam_samesource = next(iter_leads)
lead_inteam_samesource.source_id = source_xitter_post
msg_lead_inteam_samesource = self._set_won_get_rainbowman_message(lead_inteam_samesource, self.user_sales_salesman)
self.assertEqual(
msg_lead_inteam_samesource,
'Yay, your first win from Xitter Post!',
'Benoit can still receive the message as neither he nor his team have a recorded win for this source',
)

View file

@ -1,30 +1,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.tests import HttpCase
from odoo.tests.common import tagged, users
from odoo.tests import Form, HttpCase, TransactionCase
from odoo.tests.common import tagged
@tagged('post_install', '-at_install')
class TestUi(HttpCase):
class TestUi(HttpCase, TestCrmCommon):
def test_01_crm_tour(self):
# TODO: The tour is raising a JS error when selecting Brandon Freeman
# but with the demo data it succeeds to continue if there is already another lead
# in the pipe
brandon = self.env["res.partner"].create({
self.env["res.partner"].create({
'name': 'Brandon Freeman',
'email': 'brandon.freeman55@example.com',
'phone': '(355)-687-3262',
'is_company': True,
})
self.env['crm.lead'].create({
'name': "Zizizbroken",
'type': 'opportunity',
'partner_id': brandon.id,
'stage_id': self.env.ref('crm.stage_lead1').id,
'user_id': self.env.ref('base.user_admin').id,
})
self.start_tour("/web", 'crm_tour', login="admin")
self.start_tour("/odoo", 'crm_tour', login="admin")
def test_02_crm_tour_rainbowman(self):
# we create a new user to make sure they get the 'Congrats on your first deal!'
@ -33,15 +23,15 @@ class TestUi(HttpCase):
'name': 'Temporary CRM User',
'login': 'temp_crm_user',
'password': 'temp_crm_user',
'groups_id': [(6, 0, [
'group_ids': [(6, 0, [
self.ref('base.group_user'),
self.ref('sales_team.group_sale_salesman')
])]
})
self.start_tour("/web", 'crm_rainbowman', login="temp_crm_user")
self.start_tour("/odoo", 'crm_rainbowman', login="temp_crm_user")
def test_03_crm_tour_forecast(self):
self.start_tour("/web", 'crm_forecast', login="admin")
self.start_tour("/odoo", 'crm_forecast', login="admin")
def test_email_and_phone_propagation_edit_save(self):
"""Test the propagation of the email / phone on the partner.
@ -73,45 +63,145 @@ class TestUi(HttpCase):
self.assertTrue(lead.partner_email_update)
self.assertTrue(lead.partner_phone_update)
self.start_tour('/web', 'crm_email_and_phone_propagation_edit_save', login='admin')
self.start_tour('/odoo', 'crm_email_and_phone_propagation_edit_save', login='admin')
self.assertEqual(lead.email_from, 'test@example.com', 'Should not have changed the lead email')
self.assertEqual(lead.phone, '+32 494 44 44 44', 'Should not have changed the lead phone')
self.assertEqual(partner.email, 'test@example.com', 'Should have propagated the lead email on the partner')
self.assertEqual(partner.phone, '+32 494 44 44 44', 'Should have propagated the lead phone on the partner')
def test_email_and_phone_propagation_remove_email_and_phone(self):
"""Test the propagation of the email / phone on the partner.
If we remove the email and phone on the lead, it should be removed on the
partner. This test check that we correctly detect field values changes in JS
(aka undefined VS falsy).
class TestCrmKanbanUI(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.child_contact_1, cls.child_contact_2, cls.orphan_contact = cls.env['res.partner'].create([
{'name': 'Child Contact 1'}, {'name': 'Child Contact 2'}, {'name': 'Orphan Contact'},
])
cls.parent_company, cls.childless_company = cls.env['res.partner'].create([
{'name': 'Parent Company', 'is_company': True},
{'name': 'Childless Company', 'is_company': True},
])
(cls.child_contact_1 + cls.child_contact_2).parent_id = cls.parent_company
cls.quick_create_form_view = cls.env.ref('crm.quick_create_opportunity_form', raise_if_not_found=False)
def test_kanban_quick_create_form(self):
"""Check major state transitions when picking a company or a partner from the quick_create form."""
lead_form = Form(self.env['crm.lead'], self.quick_create_form_view)
self.assertFalse(lead_form._get_context('partner_id')['default_parent_id'])
lead_form.partner_id = self.orphan_contact
self.assertFalse(lead_form.commercial_partner_id)
self.assertFalse(lead_form._get_context('partner_id')['default_parent_id'])
# set contact, updates commercial partner
lead_form.partner_id = self.child_contact_1
self.assertEqual(lead_form.commercial_partner_id, self.parent_company)
self.assertEqual(lead_form._get_context('partner_id')['default_parent_id'], self.parent_company.id)
lead_form.partner_id = self.child_contact_2
self.assertEqual(lead_form.commercial_partner_id, self.parent_company)
self.assertEqual(lead_form.partner_id, self.child_contact_2)
# set company, resets partner
lead_form.commercial_partner_id = self.childless_company
self.assertEqual(lead_form.commercial_partner_id, self.childless_company)
self.assertFalse(lead_form.partner_id)
self.assertEqual(lead_form._get_context('partner_id')['default_parent_id'], self.childless_company.id)
lead_form.commercial_partner_id = self.parent_company
self.assertEqual(lead_form.commercial_partner_id, self.parent_company)
self.assertFalse(lead_form.partner_id)
self.assertEqual(lead_form._get_context('partner_id')['default_parent_id'], self.parent_company.id)
def test_kanban_quick_create_partner_inherited_details(self):
"""Check behavior of setting the quick create "company" field at create time.
It should link the company as the partner if there's no contact
and the contact details are either empty or match the company's.
Otherwise it will simply be added as the partner_name, if there is none.
"""
self.env['crm.lead'].search([]).unlink()
user_admin = self.env['res.users'].search([('login', '=', 'admin')])
partner = self.env['res.partner'].create({'name': 'Test Partner'})
lead = self.env['crm.lead'].create({
'name': 'Test Lead Propagation',
'type': 'opportunity',
'user_id': user_admin.id,
'partner_id': partner.id,
'email_from': 'test@example.com',
'phone': '+32 494 44 44 44',
no_partner = self.env['res.partner']
company = self.childless_company
company.write({
'email': 'childless@test.lan',
'phone': '+32 499 00 00 00'
})
# Check initial state
self.assertEqual(partner.email, 'test@example.com')
self.assertEqual(lead.phone, '+32 494 44 44 44')
self.assertEqual(lead.email_from, 'test@example.com')
self.assertEqual(lead.phone, '+32 494 44 44 44')
test_cases = [
({'email_from': False, 'phone': False}, {'partner_id': company, 'email_from': company.email, 'phone': company.phone}),
({'email_from': company.email, 'phone': False}, {'partner_id': no_partner, 'email_from': company.email, 'phone': False}),
(
{'email_from': company.email, 'phone': company.phone[:-1] + '1'},
{'partner_id': no_partner, 'email_from': company.email, 'phone': company.phone[:-1] + '1'},
),
(
{'email_from': company.email, 'phone': company.phone},
{'partner_id': company, 'email_from': company.email, 'phone': company.phone}
),
(
{'email_from': company.email + 'n', 'phone': company.phone},
{'partner_id': no_partner, 'email_from': company.email + 'n', 'phone': company.phone}
),
(
{'partner_id': self.child_contact_1, 'email_from': company.email, 'phone': company.phone},
{'partner_name': self.parent_company.name, 'partner_id': self.child_contact_1, 'email_from': company.email, 'phone': company.phone}
),
]
for form_values, expected_lead_values in test_cases:
lead_form = Form(self.env['crm.lead'], self.quick_create_form_view)
lead_form.commercial_partner_id = self.childless_company
self.assertFalse(lead_form.phone)
self.assertFalse(lead_form.email_from)
expected_lead_values = {'partner_name': company.name} | expected_lead_values
with self.subTest(form_values=form_values):
for field_name, input_value in form_values.items():
lead_form[field_name] = input_value
lead = lead_form.save()
for field_name, expected_value in expected_lead_values.items():
self.assertEqual(lead[field_name], expected_value)
self.assertFalse(lead.partner_email_update)
self.assertFalse(lead.partner_phone_update)
# sanity check, nothing was synced
self.assertEqual(company.email, 'childless@test.lan')
self.assertEqual(company.phone, '+32 499 00 00 00')
self.start_tour('/web', 'crm_email_and_phone_propagation_remove_email_and_phone', login='admin')
# check that it behaves reasonably if used without form too
lead = self.env['crm.lead'].create({
'commercial_partner_id': self.childless_company.id,
'name': "Childless Company's lead",
})
self.assertEqual(lead.partner_id, self.childless_company)
self.assertFalse(lead.email_from, 'Should have removed the email')
self.assertFalse(lead.phone, 'Should have removed the phone')
self.assertFalse(partner.email, 'Should have removed the email')
self.assertFalse(partner.phone, 'Should have removed the phone')
lead = self.env['crm.lead'].with_context(default_partner_id=self.parent_company).create({
'commercial_partner_id': self.childless_company.id,
'name': "Childless Company's lead",
})
self.assertEqual(lead.partner_id, self.parent_company, 'Default partner should take precedence over commercial_partner_id')
# check default_partner_id behaviors
orphan = self.orphan_contact
orphan.write({'email': 'orphan_individual@example.com', 'phone': '+32 488 00 00 00'})
child_contact = self.child_contact_1
child_contact.write({'email': 'child_contact@example.com', 'phone': '+32 477 00 00 00'})
test_cases_default = {
child_contact: {
'partner_id': child_contact, 'email_from': child_contact.email, 'phone': child_contact.phone,
'partner_name': self.parent_company.name, 'commercial_partner_id': self.parent_company
},
company: {
'partner_id': company, 'email_from': company.email, 'phone': company.phone,
'partner_name': company.name, 'commercial_partner_id': self.env['res.partner']
},
orphan: {
'partner_id': orphan, 'email_from': orphan.email, 'phone': orphan.phone,
'partner_name': False, 'commercial_partner_id': self.env['res.partner']
}
}
for default_partner, expected_lead_values in test_cases_default.items():
# check default partner in both the form and quick create form views
for view in [None, self.quick_create_form_view]:
with self.subTest(default_partner=default_partner, view=view):
lead_form = Form(self.env['crm.lead'].with_context(default_partner_id=default_partner), view)
for field_name, expected_value in expected_lead_values.items():
self.assertEqual(lead_form[field_name], expected_value)

View file

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from odoo.addons.digest.tests.common import TestDigestCommon
from odoo.tools import mute_logger
class TestCrmDigest(TestDigestCommon):
@classmethod
@mute_logger('odoo.models.unlink')
def setUpClass(cls):
super().setUpClass()
cls.env['crm.lead'].search([]).unlink()
cls.env['crm.lead'].create([{
'name': 'Lead 1',
'company_id': cls.company_1.id,
'probability': 100,
'type': 'opportunity',
'date_closed': datetime.now(),
}, {
'name': 'Lead 2',
'company_id': cls.company_1.id,
'probability': 90,
'type': 'opportunity',
'date_closed': datetime.now(),
}, {
'name': 'Lead 3',
'company_id': False,
'probability': 100,
'type': 'opportunity',
'date_closed': datetime.now(),
}, {
'name': 'Lead 4',
'company_id': cls.company_1.id,
'probability': 100,
'type': 'opportunity',
'date_closed': datetime.now() - timedelta(days=700),
}])
def test_kpi_crm_lead_created_value(self):
self.assertEqual(self.digest_1.kpi_crm_lead_created_value, 3)
self.assertEqual(self.digest_2.kpi_crm_lead_created_value, 0,
msg='This digest is in a different company')
self.assertEqual(self.digest_3.kpi_crm_lead_created_value, 3,
msg='This digest has no company, should take the current one')
self.digest_3.invalidate_recordset()
self.assertEqual(
self.digest_3.with_company(self.company_2).kpi_crm_lead_created_value,
0,
)

View file

@ -21,6 +21,13 @@ class TestLeadAssignPerf(TestLeadAssignCommon):
of random in tests.
"""
def setUp(self):
super().setUp()
# patch registry to simulate a ready environment
self.patch(self.env.registry, 'ready', True)
# we don't use mock_mail_gateway thus want to mock smtp to test the stack
self._mock_smtplib_connection()
@mute_logger('odoo.models.unlink', 'odoo.addons.crm.models.crm_team', 'odoo.addons.crm.models.crm_team_member')
def test_assign_perf_duplicates(self):
""" Test assign process with duplicates on partner. Allow to ensure notably
@ -47,11 +54,11 @@ class TestLeadAssignPerf(TestLeadAssignCommon):
# commit probability and related fields
leads.flush_recordset()
# randomness: at least 1 query
# randomness: at least 1 query, +3 for demo -> 957 + 5
with self.with_user('user_sales_manager'):
self.env['res.users'].has_group('base.group_user') # warmup the cache to avoid inconsistency between community an enterprise
with self.assertQueryCount(user_sales_manager=1266): # crm 1187
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2)
self.env.user._is_internal() # warmup the cache to avoid inconsistency between community an enterprise
with self.assertQueryCount(user_sales_manager=962):
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads()
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
@ -62,12 +69,12 @@ class TestLeadAssignPerf(TestLeadAssignCommon):
self.assertEqual(len(leads_st1) + len(leads_stc), len(leads)) # Make sure all lead are assigned
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 11) # 45 max on 2 days (3) + compensation (8.4)
self.assertMemberAssign(self.sales_team_1_m2, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_1_m3, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_convert_m1, 8) # 30 max on 15 (2) + compensation (5.6)
self.assertMemberAssign(self.sales_team_convert_m2, 15) # 60 max on 15 (4) + compsantion (11.2)
self.members.invalidate_model(['lead_month_count', 'lead_day_count'])
self.assertMemberAssign(self.sales_team_1_m1, 2) # 45 max on one month -> 2 daily
self.assertMemberAssign(self.sales_team_1_m2, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_1_m3, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m1, 1) # 30 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m2, 2) # 60 max on one month -> 2 daily
@mute_logger('odoo.models.unlink', 'odoo.addons.crm.models.crm_team', 'odoo.addons.crm.models.crm_team_member')
def test_assign_perf_no_duplicates(self):
@ -93,10 +100,10 @@ class TestLeadAssignPerf(TestLeadAssignCommon):
# commit probability and related fields
leads.flush_recordset()
# randomness: at least 1 query
# randomness: at least 1 query, +1 for demo
with self.with_user('user_sales_manager'):
with self.assertQueryCount(user_sales_manager=585): # crm 584
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2)
with self.assertQueryCount(user_sales_manager=552):
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads()
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
@ -105,12 +112,12 @@ class TestLeadAssignPerf(TestLeadAssignCommon):
self.assertEqual(len(leads_st1) + len(leads_stc), 100)
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 11) # 45 max on 2 days (3) + compensation (8.4)
self.assertMemberAssign(self.sales_team_1_m2, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_1_m3, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_convert_m1, 8) # 30 max on 15 (2) + compensation (5.6)
self.assertMemberAssign(self.sales_team_convert_m2, 15) # 60 max on 15 (4) + compensation (11.2)
self.members.invalidate_model(['lead_month_count', 'lead_day_count'])
self.assertMemberAssign(self.sales_team_1_m1, 2) # 45 max on one month -> 2 daily
self.assertMemberAssign(self.sales_team_1_m2, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_1_m3, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m1, 1) # 30 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m2, 2) # 60 max on one month -> 2 daily
@mute_logger('odoo.models.unlink', 'odoo.addons.crm.models.crm_team', 'odoo.addons.crm.models.crm_team_member')
def test_assign_perf_populated(self):
@ -176,10 +183,10 @@ class TestLeadAssignPerf(TestLeadAssignCommon):
# commit probability and related fields
leads.flush_recordset()
# randomness
# randomness: add 2 queries
with self.with_user('user_sales_manager'):
with self.assertQueryCount(user_sales_manager=6280): # crm 6226 / com 6276 / ent 6278
self.env['crm.team'].browse(sales_teams.ids)._action_assign_leads(work_days=30)
with self.assertQueryCount(user_sales_manager=5173):
self.env['crm.team'].browse(sales_teams.ids)._action_assign_leads()
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)])
@ -187,12 +194,12 @@ class TestLeadAssignPerf(TestLeadAssignCommon):
self.assertEqual(leads.user_id, sales_teams.member_ids)
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 45) # 45 max on one month
self.assertMemberAssign(self.sales_team_1_m2, 15) # 15 max on one month
self.assertMemberAssign(self.sales_team_1_m3, 15) # 15 max on one month
self.assertMemberAssign(self.sales_team_convert_m1, 30) # 30 max on one month
self.assertMemberAssign(self.sales_team_convert_m2, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m1, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m2, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m3, 15) # 15 max on one month
self.members.invalidate_model(['lead_month_count', 'lead_day_count'])
self.assertMemberAssign(self.sales_team_1_m1, 2) # 45 max on one month -> 2 daily
self.assertMemberAssign(self.sales_team_1_m2, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_1_m3, 1) # 15 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m1, 1) # 30 max on one month -> 1 daily
self.assertMemberAssign(self.sales_team_convert_m2, 2) # 60 max on one month -> 2 daily
self.assertMemberAssign(sales_team_3_m1, 2) # 60 max on one month -> 2 daily
self.assertMemberAssign(sales_team_3_m2, 2) # 60 max on one month -> 2 daily
self.assertMemberAssign(sales_team_3_m3, 1) # 15 max on one month -> 1 daily

View file

@ -1,71 +1,73 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.tests.common import Form
from odoo.tests import tagged, users
@tagged('res_partner')
class TestPartner(TestCrmCommon):
# This is explicit: we want CRM only check, to test base method
@tagged('res_partner', '-post_install', 'at_install')
class TestResPartner(TestCrmCommon):
@users('user_sales_leads')
def test_parent_sync_sales_rep(self):
""" Test team_id / user_id sync from parent to children if the contact
is a person. Company children are not updated. """
contact_company = self.contact_company.with_env(self.env)
contact_company_1 = self.contact_company_1.with_env(self.env)
self.assertFalse(contact_company.team_id)
self.assertFalse(contact_company.user_id)
self.assertFalse(contact_company_1.team_id)
self.assertFalse(contact_company_1.user_id)
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.contact_1_1, cls.contact_1_2 = cls.env['res.partner'].create([
{
'name': 'Philip J Fry Bouffe-tête',
'email': 'bouffe.tete@test.example.com',
'function': 'Bouffe-Tête',
'lang': cls.lang_en.code,
'phone': False,
'parent_id': cls.contact_1.id,
'is_company': False,
'street': 'Same as Fry',
'city': 'New York',
'country_id': cls.env.ref('base.us').id,
'zip': '54321',
}, {
'name': 'Philip J Fry Banjo',
'email': 'banjo@test.example.com',
'function': 'Being a banjo',
'lang': cls.lang_en.code,
'phone': False,
'parent_id': cls.contact_1.id,
'is_company': False,
'street': 'Same as Fry',
'city': 'New York',
'country_id': cls.env.ref('base.us').id,
'zip': '54321',
}
])
child = self.contact_1.with_env(self.env)
self.assertEqual(child.parent_id, self.contact_company_1)
self.assertFalse(child.team_id)
self.assertFalse(child.user_id)
cls.test_leads = cls.env['crm.lead'].create([
{
'name': 'CompanyLead',
'type': 'lead',
'partner_id': cls.contact_company_1.id,
}, {
'name': 'ChildLead',
'type': 'lead',
'partner_id': cls.contact_1.id,
}, {
'name': 'GrandChildLead',
'type': 'lead',
'partner_id': cls.contact_1_1.id,
}, {
'name': 'GrandChildOpp',
'type': 'opportunity',
'partner_id': cls.contact_1_1.id,
}, {
'name': 'Nobody',
'type': 'opportunity',
},
])
# update comppany sales rep info
contact_company.user_id = self.env.uid
contact_company.team_id = self.sales_team_1.id
# change child parent: shold update sales rep info
child.parent_id = contact_company.id
self.assertEqual(child.user_id, self.env.user)
# test form tool
# <field name="team_id" groups="base.group_no_one"/>
with self.debug_mode():
partner_form = Form(self.env['res.partner'], 'base.view_partner_form')
partner_form.parent_id = contact_company
partner_form.company_type = 'person'
partner_form.name = 'Hermes Conrad'
self.assertEqual(partner_form.team_id, self.sales_team_1)
self.assertEqual(partner_form.user_id, self.env.user)
partner_form.parent_id = contact_company_1
self.assertEqual(partner_form.team_id, self.sales_team_1)
self.assertEqual(partner_form.user_id, self.env.user)
# test form tool
# <field name="team_id" groups="base.group_no_one"/>
with self.debug_mode():
partner_form = Form(self.env['res.partner'], 'base.view_partner_form')
# `parent_id` is invisible when `is_company` is True (`company_type == 'company'`)
# and parent_id is not set
# So, set a temporary `parent_id` before setting the contact as company
# to make `parent_id` visible in the interface while being a company
# <field name="parent_id"
# attrs="{
# 'invisible': [
# '|',
# '&amp;', ('is_company','=', True),('parent_id', '=', False),
# ('company_name', '!=', False),('company_name', '!=', '')
# ]
# }"
# />
partner_form.parent_id = contact_company_1
partner_form.company_type = 'company'
partner_form.parent_id = contact_company
partner_form.name = 'Mom Corp'
self.assertFalse(partner_form.team_id)
self.assertFalse(partner_form.user_id)
@users('user_sales_manager')
def test_fields_opportunity_count(self):
(
contact_company_1, contact_1, contact_1_1, contact_1_2
) = (
self.contact_company_1 + self.contact_1 + self.contact_1_1 + self.contact_1_2
).with_env(self.env)
self.assertEqual(contact_company_1.opportunity_count, 4, 'Should contain own + children leads')
self.assertEqual(contact_1.opportunity_count, 3, 'Should contain own + child leads')
self.assertEqual(contact_1_1.opportunity_count, 2, 'Should contain own, aka 2')
self.assertEqual(contact_1_2.opportunity_count, 0, 'Should contain own, aka none')

View file

@ -13,7 +13,7 @@ class TestUi(HttpCase, SalesTeamCommon):
@users('salesmanager')
def test_crm_team_members_mono_company(self):
""" Make sure you can create crm.team records with members in a mono-company scenario """
self.sale_manager.sudo().groups_id -= self.env.ref("base.group_multi_company")
self.sale_manager.sudo().group_ids -= self.env.ref("base.group_multi_company")
self.env['ir.config_parameter'].sudo().set_param('sales_team.membership_multi', True)
self.start_tour("/", "create_crm_team_tour", login="salesmanager")