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

@ -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()