test_crm_pls.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from datetime import timedelta
  4. from odoo import tools
  5. from odoo.addons.mail.tests.common import mail_new_test_user
  6. from odoo.fields import Date
  7. from odoo.tests import Form, tagged, users
  8. from odoo.tests.common import TransactionCase
  9. @tagged('crm_lead_pls')
  10. class TestCRMPLS(TransactionCase):
  11. @classmethod
  12. def setUpClass(cls):
  13. """ Keep a limited setup to ensure tests are not impacted by other
  14. records created in CRM common. """
  15. super(TestCRMPLS, cls).setUpClass()
  16. cls.company_main = cls.env.user.company_id
  17. cls.user_sales_manager = mail_new_test_user(
  18. cls.env, login='user_sales_manager',
  19. name='Martin PLS Sales Manager', email='crm_manager@test.example.com',
  20. company_id=cls.company_main.id,
  21. notification_type='inbox',
  22. groups='sales_team.group_sale_manager,base.group_partner_manager',
  23. )
  24. cls.pls_team = cls.env['crm.team'].create({
  25. 'name': 'PLS Team',
  26. })
  27. def _get_lead_values(self, team_id, name_suffix, country_id, state_id, email_state, phone_state, source_id, stage_id):
  28. return {
  29. 'name': 'lead_' + name_suffix,
  30. 'type': 'opportunity',
  31. 'state_id': state_id,
  32. 'email_state': email_state,
  33. 'phone_state': phone_state,
  34. 'source_id': source_id,
  35. 'stage_id': stage_id,
  36. 'country_id': country_id,
  37. 'team_id': team_id
  38. }
  39. def generate_leads_with_tags(self, tag_ids):
  40. Lead = self.env['crm.lead']
  41. team_id = self.env['crm.team'].create({
  42. 'name': 'blup',
  43. }).id
  44. leads_to_create = []
  45. for i in range(150):
  46. if i < 50: # tag 1
  47. leads_to_create.append({
  48. 'name': 'lead_tag_%s' % str(i),
  49. 'tag_ids': [(4, tag_ids[0])],
  50. 'team_id': team_id
  51. })
  52. elif i < 100: # tag 2
  53. leads_to_create.append({
  54. 'name': 'lead_tag_%s' % str(i),
  55. 'tag_ids': [(4, tag_ids[1])],
  56. 'team_id': team_id
  57. })
  58. else: # tag 1 and 2
  59. leads_to_create.append({
  60. 'name': 'lead_tag_%s' % str(i),
  61. 'tag_ids': [(6, 0, tag_ids)],
  62. 'team_id': team_id
  63. })
  64. leads_with_tags = Lead.create(leads_to_create)
  65. return leads_with_tags
  66. def test_crm_lead_pls_update(self):
  67. """ We test here that the wizard for updating probabilities from settings
  68. is getting correct value from config params and after updating values
  69. from the wizard, the config params are correctly updated
  70. """
  71. # Set the PLS config
  72. frequency_fields = self.env['crm.lead.scoring.frequency.field'].search([])
  73. pls_fields_str = ','.join(frequency_fields.mapped('field_id.name'))
  74. pls_start_date_str = "2021-01-01"
  75. IrConfigSudo = self.env['ir.config_parameter'].sudo()
  76. IrConfigSudo.set_param("crm.pls_start_date", pls_start_date_str)
  77. IrConfigSudo.set_param("crm.pls_fields", pls_fields_str)
  78. date_to_update = "2021-02-02"
  79. fields_to_remove = frequency_fields.filtered(lambda f: f.field_id.name in ['source_id', 'lang_id'])
  80. fields_after_updation_str = ','.join((frequency_fields - fields_to_remove).mapped('field_id.name'))
  81. # Check that wizard to update lead probabilities has correct value set by default
  82. pls_update_wizard = Form(self.env['crm.lead.pls.update'])
  83. with pls_update_wizard:
  84. self.assertEqual(Date.to_string(pls_update_wizard.pls_start_date), pls_start_date_str, 'Correct date is taken from config')
  85. self.assertEqual(','.join([f.field_id.name for f in pls_update_wizard.pls_fields]), pls_fields_str, 'Correct fields are taken from config')
  86. # Update the wizard values and check that config values and probabilities are updated accordingly
  87. pls_update_wizard.pls_start_date = date_to_update
  88. for field in fields_to_remove:
  89. pls_update_wizard.pls_fields.remove(field.id)
  90. pls_update_wizard0 = pls_update_wizard.save()
  91. pls_update_wizard0.action_update_crm_lead_probabilities()
  92. # Config params should have been updated
  93. self.assertEqual(IrConfigSudo.get_param("crm.pls_start_date"), date_to_update, 'Correct date is updated in config')
  94. self.assertEqual(IrConfigSudo.get_param("crm.pls_fields"), fields_after_updation_str, 'Correct fields are updated in config')
  95. def test_predictive_lead_scoring(self):
  96. """ We test here computation of lead probability based on PLS Bayes.
  97. We will use 3 different values for each possible variables:
  98. country_id : 1,2,3
  99. state_id: 1,2,3
  100. email_state: correct, incorrect, None
  101. phone_state: correct, incorrect, None
  102. source_id: 1,2,3
  103. stage_id: 1,2,3 + the won stage
  104. And we will compute all of this for 2 different team_id
  105. Note : We assume here that original bayes computation is correct
  106. as we don't compute manually the probabilities."""
  107. Lead = self.env['crm.lead']
  108. LeadScoringFrequency = self.env['crm.lead.scoring.frequency']
  109. state_values = ['correct', 'incorrect', None]
  110. source_ids = self.env['utm.source'].search([], limit=3).ids
  111. state_ids = self.env['res.country.state'].search([], limit=3).ids
  112. country_ids = self.env['res.country'].search([], limit=3).ids
  113. stage_ids = self.env['crm.stage'].search([], limit=3).ids
  114. won_stage_id = self.env['crm.stage'].search([('is_won', '=', True)], limit=1).id
  115. team_ids = self.env['crm.team'].create([{'name': 'Team Test 1'}, {'name': 'Team Test 2'}, {'name': 'Team Test 3'}]).ids
  116. # create bunch of lost and won crm_lead
  117. leads_to_create = []
  118. # for team 1
  119. for i in range(3):
  120. leads_to_create.append(
  121. 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]))
  122. leads_to_create.append(
  123. 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]))
  124. leads_to_create.append(
  125. 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]))
  126. # for team 2
  127. leads_to_create.append(
  128. 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]))
  129. leads_to_create.append(
  130. 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]))
  131. leads_to_create.append(
  132. 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]))
  133. leads_to_create.append(
  134. 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]))
  135. leads_to_create.append(
  136. 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]))
  137. # for leads with no team
  138. leads_to_create.append(
  139. 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]))
  140. leads_to_create.append(
  141. 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]))
  142. leads_to_create.append(
  143. 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]))
  144. leads_to_create.append(
  145. 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]))
  146. leads = Lead.create(leads_to_create)
  147. # assign team 3 to all leads with no teams (also take data into account).
  148. leads_with_no_team = self.env['crm.lead'].sudo().search([('team_id', '=', False)])
  149. leads_with_no_team.write({'team_id': team_ids[2]})
  150. # Set the PLS config
  151. self.env['ir.config_parameter'].sudo().set_param("crm.pls_start_date", "2000-01-01")
  152. self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", "country_id,state_id,email_state,phone_state,source_id,tag_ids")
  153. # set leads as won and lost
  154. # for Team 1
  155. leads[0].action_set_lost()
  156. leads[1].action_set_lost()
  157. leads[2].action_set_won()
  158. # for Team 2
  159. leads[5].action_set_lost()
  160. leads[6].action_set_lost()
  161. leads[7].action_set_won()
  162. # Leads with no team
  163. leads[10].action_set_won()
  164. leads[11].action_set_lost()
  165. leads[12].action_set_lost()
  166. # A. Test Full Rebuild
  167. # rebuild frequencies table and recompute automated_probability for all leads.
  168. Lead._cron_update_automated_probabilities()
  169. # As the cron is computing and writing in SQL queries, we need to invalidate the cache
  170. self.env.invalidate_all()
  171. self.assertEqual(tools.float_compare(leads[3].automated_probability, 33.49, 2), 0)
  172. self.assertEqual(tools.float_compare(leads[8].automated_probability, 7.74, 2), 0)
  173. lead_13_team_3_proba = leads[13].automated_probability
  174. self.assertEqual(tools.float_compare(lead_13_team_3_proba, 35.09, 2), 0)
  175. # Probability for Lead with no teams should be based on all the leads no matter their team.
  176. # De-assign team 3 and rebuilt frequency table and recompute.
  177. # Proba should be different as "no team" is not considered as a separated team.
  178. leads_with_no_team.write({'team_id': False})
  179. Lead._cron_update_automated_probabilities()
  180. lead_13_no_team_proba = leads[13].automated_probability
  181. self.assertTrue(lead_13_team_3_proba != leads[13].automated_probability, "Probability for leads with no team should be different than if they where in their own team.")
  182. self.assertEqual(tools.float_compare(lead_13_no_team_proba, 36.65, 2), 0)
  183. # Test frequencies
  184. lead_4_stage_0_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', stage_ids[0])])
  185. lead_4_stage_won_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', won_stage_id)])
  186. lead_4_country_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[4].country_id.id)])
  187. lead_4_email_state_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'email_state'), ('value', '=', str(leads[4].email_state))])
  188. lead_9_stage_0_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', stage_ids[0])])
  189. lead_9_stage_won_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', won_stage_id)])
  190. lead_9_country_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[9].country_id.id)])
  191. lead_9_email_state_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'email_state'), ('value', '=', str(leads[9].email_state))])
  192. self.assertEqual(lead_4_stage_0_freq.won_count, 1.1)
  193. self.assertEqual(lead_4_stage_won_freq.won_count, 1.1)
  194. self.assertEqual(lead_4_country_freq.won_count, 0.1)
  195. self.assertEqual(lead_4_email_state_freq.won_count, 1.1)
  196. self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1)
  197. self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1)
  198. self.assertEqual(lead_4_country_freq.lost_count, 1.1)
  199. self.assertEqual(lead_4_email_state_freq.lost_count, 2.1)
  200. self.assertEqual(lead_9_stage_0_freq.won_count, 1.1)
  201. self.assertEqual(lead_9_stage_won_freq.won_count, 1.1)
  202. self.assertEqual(lead_9_country_freq.won_count, 0.0) # frequency does not exist
  203. self.assertEqual(lead_9_email_state_freq.won_count, 1.1)
  204. self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1)
  205. self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1)
  206. self.assertEqual(lead_9_country_freq.lost_count, 0.0) # frequency does not exist
  207. self.assertEqual(lead_9_email_state_freq.lost_count, 2.1)
  208. # B. Test Live Increment
  209. leads[4].action_set_lost()
  210. leads[9].action_set_won()
  211. # re-get frequencies that did not exists before
  212. lead_9_country_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[9].country_id.id)])
  213. # B.1. Test frequencies - team 1 should not impact team 2
  214. self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
  215. self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
  216. self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
  217. self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
  218. self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # + 1
  219. self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) # unchanged - consider stages with <= sequence when lost
  220. self.assertEqual(lead_4_country_freq.lost_count, 2.1) # + 1
  221. self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # + 1
  222. self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # + 1
  223. self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # + 1 - consider every stages when won
  224. self.assertEqual(lead_9_country_freq.won_count, 1.1) # + 1
  225. self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # + 1
  226. self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged
  227. self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged
  228. self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged (did not exists before)
  229. self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged
  230. # Propabilities of other leads should not be impacted as only modified lead are recomputed.
  231. self.assertEqual(tools.float_compare(leads[3].automated_probability, 33.49, 2), 0)
  232. self.assertEqual(tools.float_compare(leads[8].automated_probability, 7.74, 2), 0)
  233. self.assertEqual(leads[3].is_automated_probability, True)
  234. self.assertEqual(leads[8].is_automated_probability, True)
  235. # Restore -> Should decrease lost
  236. leads[4].toggle_active()
  237. self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
  238. self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
  239. self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
  240. self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
  241. self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # - 1
  242. self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) # unchanged - consider stages with <= sequence when lost
  243. self.assertEqual(lead_4_country_freq.lost_count, 1.1) # - 1
  244. self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # - 1
  245. self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # unchanged
  246. self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # unchanged
  247. self.assertEqual(lead_9_country_freq.won_count, 1.1) # unchanged
  248. self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # unchanged
  249. self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged
  250. self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged
  251. self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged
  252. self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged
  253. # set to won stage -> Should increase won
  254. leads[4].stage_id = won_stage_id
  255. self.assertEqual(lead_4_stage_0_freq.won_count, 2.1) # + 1
  256. self.assertEqual(lead_4_stage_won_freq.won_count, 2.1) # + 1
  257. self.assertEqual(lead_4_country_freq.won_count, 1.1) # + 1
  258. self.assertEqual(lead_4_email_state_freq.won_count, 2.1) # + 1
  259. self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # unchanged
  260. self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) # unchanged
  261. self.assertEqual(lead_4_country_freq.lost_count, 1.1) # unchanged
  262. self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # unchanged
  263. # Archive (was won, now lost) -> Should decrease won and increase lost
  264. leads[4].toggle_active()
  265. self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # - 1
  266. self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # - 1
  267. self.assertEqual(lead_4_country_freq.won_count, 0.1) # - 1
  268. self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # - 1
  269. self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # + 1
  270. 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
  271. self.assertEqual(lead_4_country_freq.lost_count, 2.1) # + 1
  272. self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # + 1
  273. # Move to original stage -> Should do nothing (as lead is still lost)
  274. leads[4].stage_id = stage_ids[0]
  275. self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
  276. self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
  277. self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
  278. self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
  279. self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # unchanged
  280. self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # unchanged
  281. self.assertEqual(lead_4_country_freq.lost_count, 2.1) # unchanged
  282. self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # unchanged
  283. # Restore -> Should decrease lost - at the end, frequencies should be like first frequencyes tests (except for 0.0 -> 0.1)
  284. leads[4].toggle_active()
  285. self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
  286. self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
  287. self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
  288. self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
  289. self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # - 1
  290. self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # unchanged - consider stages with <= sequence when lost
  291. self.assertEqual(lead_4_country_freq.lost_count, 1.1) # - 1
  292. self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # - 1
  293. # Probabilities should only be recomputed after modifying the lead itself.
  294. leads[3].stage_id = stage_ids[0] # probability should only change a bit as frequencies are almost the same (except 0.0 -> 0.1)
  295. leads[8].stage_id = stage_ids[0] # probability should change quite a lot
  296. # Test frequencies (should not have changed)
  297. self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
  298. self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
  299. self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
  300. self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
  301. self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # unchanged
  302. self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # unchanged
  303. self.assertEqual(lead_4_country_freq.lost_count, 1.1) # unchanged
  304. self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # unchanged
  305. self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # unchanged
  306. self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # unchanged
  307. self.assertEqual(lead_9_country_freq.won_count, 1.1) # unchanged
  308. self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # unchanged
  309. self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged
  310. self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged
  311. self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged
  312. self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged
  313. # Continue to test probability computation
  314. leads[3].probability = 40
  315. self.assertEqual(leads[3].is_automated_probability, False)
  316. self.assertEqual(leads[8].is_automated_probability, True)
  317. self.assertEqual(tools.float_compare(leads[3].automated_probability, 20.87, 2), 0)
  318. self.assertEqual(tools.float_compare(leads[8].automated_probability, 2.43, 2), 0)
  319. self.assertEqual(tools.float_compare(leads[3].probability, 40, 2), 0)
  320. self.assertEqual(tools.float_compare(leads[8].probability, 2.43, 2), 0)
  321. # Test modify country_id
  322. leads[8].country_id = country_ids[1]
  323. self.assertEqual(tools.float_compare(leads[8].automated_probability, 34.38, 2), 0)
  324. self.assertEqual(tools.float_compare(leads[8].probability, 34.38, 2), 0)
  325. leads[8].country_id = country_ids[0]
  326. self.assertEqual(tools.float_compare(leads[8].automated_probability, 2.43, 2), 0)
  327. self.assertEqual(tools.float_compare(leads[8].probability, 2.43, 2), 0)
  328. # ----------------------------------------------
  329. # Test tag_id frequencies and probability impact
  330. # ----------------------------------------------
  331. tag_ids = self.env['crm.tag'].create([
  332. {'name': "Tag_test_1"},
  333. {'name': "Tag_test_2"},
  334. ]).ids
  335. # tag_ids = self.env['crm.tag'].search([], limit=2).ids
  336. leads_with_tags = self.generate_leads_with_tags(tag_ids)
  337. leads_with_tags[:30].action_set_lost() # 60% lost on tag 1
  338. leads_with_tags[31:50].action_set_won() # 40% won on tag 1
  339. leads_with_tags[50:90].action_set_lost() # 80% lost on tag 2
  340. leads_with_tags[91:100].action_set_won() # 20% won on tag 2
  341. leads_with_tags[100:135].action_set_lost() # 70% lost on tag 1 and 2
  342. leads_with_tags[136:150].action_set_won() # 30% won on tag 1 and 2
  343. # tag 1 : won = 19+14 / lost = 30+35
  344. # tag 2 : won = 9+14 / lost = 40+35
  345. tag_1_freq = LeadScoringFrequency.search([('variable', '=', 'tag_id'), ('value', '=', tag_ids[0])])
  346. tag_2_freq = LeadScoringFrequency.search([('variable', '=', 'tag_id'), ('value', '=', tag_ids[1])])
  347. self.assertEqual(tools.float_compare(tag_1_freq.won_count, 33.1, 1), 0)
  348. self.assertEqual(tools.float_compare(tag_1_freq.lost_count, 65.1, 1), 0)
  349. self.assertEqual(tools.float_compare(tag_2_freq.won_count, 23.1, 1), 0)
  350. self.assertEqual(tools.float_compare(tag_2_freq.lost_count, 75.1, 1), 0)
  351. # Force recompute - A priori, no need to do this as, for each won / lost, we increment tag frequency.
  352. Lead._cron_update_automated_probabilities()
  353. self.env.invalidate_all()
  354. lead_tag_1 = leads_with_tags[30]
  355. lead_tag_2 = leads_with_tags[90]
  356. lead_tag_1_2 = leads_with_tags[135]
  357. self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 33.69, 2), 0)
  358. self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0)
  359. self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 28.05, 2), 0)
  360. lead_tag_1.tag_ids = [(5, 0, 0)] # remove all tags
  361. lead_tag_1_2.tag_ids = [(3, tag_ids[1], 0)] # remove tag 2
  362. self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0)
  363. self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0) # no impact
  364. self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 33.69, 2), 0)
  365. lead_tag_1.tag_ids = [(4, tag_ids[1])] # add tag 2
  366. lead_tag_2.tag_ids = [(4, tag_ids[0])] # add tag 1
  367. lead_tag_1_2.tag_ids = [(3, tag_ids[0]), (4, tag_ids[1])] # remove tag 1 / add tag 2
  368. self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 23.51, 2), 0)
  369. self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 28.05, 2), 0)
  370. self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 23.51, 2), 0)
  371. # go back to initial situation
  372. lead_tag_1.tag_ids = [(3, tag_ids[1]), (4, tag_ids[0])] # remove tag 2 / add tag 1
  373. lead_tag_2.tag_ids = [(3, tag_ids[0])] # remove tag 1
  374. lead_tag_1_2.tag_ids = [(4, tag_ids[0])] # add tag 1
  375. self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 33.69, 2), 0)
  376. self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0)
  377. self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 28.05, 2), 0)
  378. # set email_state for each lead and update probabilities
  379. leads.filtered(lambda lead: lead.id % 2 == 0).email_state = 'correct'
  380. leads.filtered(lambda lead: lead.id % 2 == 1).email_state = 'incorrect'
  381. Lead._cron_update_automated_probabilities()
  382. self.env.invalidate_all()
  383. self.assertEqual(tools.float_compare(leads[3].automated_probability, 4.21, 2), 0)
  384. self.assertEqual(tools.float_compare(leads[8].automated_probability, 0.23, 2), 0)
  385. # remove all pls fields
  386. self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", False)
  387. Lead._cron_update_automated_probabilities()
  388. self.env.invalidate_all()
  389. self.assertEqual(tools.float_compare(leads[3].automated_probability, 34.38, 2), 0)
  390. self.assertEqual(tools.float_compare(leads[8].automated_probability, 50.0, 2), 0)
  391. # check if the probabilities are the same with the old param
  392. self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", "country_id,state_id,email_state,phone_state,source_id")
  393. Lead._cron_update_automated_probabilities()
  394. self.env.invalidate_all()
  395. self.assertEqual(tools.float_compare(leads[3].automated_probability, 4.21, 2), 0)
  396. self.assertEqual(tools.float_compare(leads[8].automated_probability, 0.23, 2), 0)
  397. # remove tag_ids from the calculation
  398. self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0)
  399. self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 28.6, 2), 0)
  400. self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 28.6, 2), 0)
  401. lead_tag_1.tag_ids = [(5, 0, 0)] # remove all tags
  402. lead_tag_2.tag_ids = [(4, tag_ids[0])] # add tag 1
  403. lead_tag_1_2.tag_ids = [(3, tag_ids[1], 0)] # remove tag 2
  404. self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0)
  405. self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 28.6, 2), 0)
  406. self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 28.6, 2), 0)
  407. def test_predictive_lead_scoring_always_won(self):
  408. """ The computation may lead scores close to 100% (or 0%), we check that pending
  409. leads are always in the ]0-100[ range."""
  410. Lead = self.env['crm.lead']
  411. LeadScoringFrequency = self.env['crm.lead.scoring.frequency']
  412. country_id = self.env['res.country'].search([], limit=1).id
  413. stage_id = self.env['crm.stage'].search([], limit=1).id
  414. team_id = self.env['crm.team'].create({'name': 'Team Test 1'}).id
  415. # create two leads
  416. leads = Lead.create([
  417. self._get_lead_values(team_id, 'edge pending', country_id, False, False, False, False, stage_id),
  418. self._get_lead_values(team_id, 'edge lost', country_id, False, False, False, False, stage_id),
  419. self._get_lead_values(team_id, 'edge won', country_id, False, False, False, False, stage_id),
  420. ])
  421. # set a new tag
  422. leads.tag_ids = self.env['crm.tag'].create({'name': 'lead scoring edge case'})
  423. # Set the PLS config
  424. self.env['ir.config_parameter'].sudo().set_param("crm.pls_start_date", "2000-01-01")
  425. # tag_ids can be used in versions newer than v14
  426. self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", "country_id")
  427. # set leads as won and lost
  428. leads[1].action_set_lost()
  429. leads[2].action_set_won()
  430. # recompute
  431. Lead._cron_update_automated_probabilities()
  432. self.env.invalidate_all()
  433. # adapt the probability frequency to have high values
  434. # this way we are nearly sure it's going to be won
  435. freq_stage = LeadScoringFrequency.search([('variable', '=', 'stage_id'), ('value', '=', str(stage_id))])
  436. freq_tag = LeadScoringFrequency.search([('variable', '=', 'tag_id'), ('value', '=', str(leads.tag_ids.id))])
  437. freqs = freq_stage + freq_tag
  438. # check probabilities: won edge case
  439. freqs.write({'won_count': 10000000, 'lost_count': 1})
  440. leads._compute_probabilities()
  441. self.assertEqual(tools.float_compare(leads[2].probability, 100, 2), 0)
  442. self.assertEqual(tools.float_compare(leads[1].probability, 0, 2), 0)
  443. self.assertEqual(tools.float_compare(leads[0].probability, 99.99, 2), 0)
  444. # check probabilities: lost edge case
  445. freqs.write({'won_count': 1, 'lost_count': 10000000})
  446. leads._compute_probabilities()
  447. self.assertEqual(tools.float_compare(leads[2].probability, 100, 2), 0)
  448. self.assertEqual(tools.float_compare(leads[1].probability, 0, 2), 0)
  449. self.assertEqual(tools.float_compare(leads[0].probability, 0.01, 2), 0)
  450. def test_settings_pls_start_date(self):
  451. # We test here that settings never crash due to ill-configured config param 'crm.pls_start_date'
  452. set_param = self.env['ir.config_parameter'].sudo().set_param
  453. str_date_8_days_ago = Date.to_string(Date.today() - timedelta(days=8))
  454. resConfig = self.env['res.config.settings']
  455. set_param("crm.pls_start_date", "2021-10-10")
  456. res_config_new = resConfig.new()
  457. self.assertEqual(Date.to_string(res_config_new.predictive_lead_scoring_start_date),
  458. "2021-10-10", "If config param is a valid date, date in settings should match with config param")
  459. set_param("crm.pls_start_date", "")
  460. res_config_new = resConfig.new()
  461. self.assertEqual(Date.to_string(res_config_new.predictive_lead_scoring_start_date),
  462. str_date_8_days_ago, "If config param is empty, date in settings should be set to 8 days before today")
  463. set_param("crm.pls_start_date", "One does not simply walk into system parameters to corrupt them")
  464. res_config_new = resConfig.new()
  465. self.assertEqual(Date.to_string(res_config_new.predictive_lead_scoring_start_date),
  466. str_date_8_days_ago, "If config param is not a valid date, date in settings should be set to 8 days before today")
  467. def test_pls_no_share_stage(self):
  468. """ We test here the situation where all stages are team specific, as there is
  469. a current limitation (can be seen in _pls_get_won_lost_total_count) regarding
  470. the first stage (used to know how many lost and won there is) that requires
  471. to have no team assigned to it."""
  472. Lead = self.env['crm.lead']
  473. team_id = self.env['crm.team'].create([{'name': 'Team Test'}]).id
  474. self.env['crm.stage'].search([('team_id', '=', False)]).write({'team_id': team_id})
  475. lead = Lead.create({'name': 'team', 'team_id': team_id, 'probability': 41.23})
  476. Lead._cron_update_automated_probabilities()
  477. self.assertEqual(tools.float_compare(lead.probability, 41.23, 2), 0)
  478. self.assertEqual(tools.float_compare(lead.automated_probability, 0, 2), 0)
  479. @users('user_sales_manager')
  480. def test_team_unlink(self):
  481. """ Test that frequencies are sent to "no team" when unlinking a team
  482. in order to avoid losing too much informations. """
  483. pls_team = self.env["crm.team"].browse(self.pls_team.ids)
  484. # clean existing data
  485. self.env["crm.lead.scoring.frequency"].sudo().search([('team_id', '=', False)]).unlink()
  486. # existing no-team data
  487. no_team = [
  488. ('stage_id', '1', 20, 10),
  489. ('stage_id', '2', 0.1, 0.1),
  490. ('stage_id', '3', 10, 0),
  491. ('country_id', '1', 10, 0.1),
  492. ]
  493. self.env["crm.lead.scoring.frequency"].sudo().create([
  494. {'variable': variable, 'value': value,
  495. 'won_count': won_count, 'lost_count': lost_count,
  496. 'team_id': False,
  497. } for variable, value, won_count, lost_count in no_team
  498. ])
  499. # add some frequencies to team to unlink
  500. team = [
  501. ('stage_id', '1', 20, 10), # existing noteam
  502. ('country_id', '1', 0.1, 10), # existing noteam
  503. ('country_id', '2', 0.1, 0), # new but void
  504. ('country_id', '3', 30, 30), # new
  505. ]
  506. existing_plsteam = self.env["crm.lead.scoring.frequency"].sudo().create([
  507. {'variable': variable, 'value': value,
  508. 'won_count': won_count, 'lost_count': lost_count,
  509. 'team_id': pls_team.id,
  510. } for variable, value, won_count, lost_count in team
  511. ])
  512. pls_team.unlink()
  513. final_noteam = [
  514. ('stage_id', '1', 40, 20),
  515. ('stage_id', '2', 0.1, 0.1),
  516. ('stage_id', '3', 10, 0),
  517. ('country_id', '1', 10, 10),
  518. ('country_id', '3', 30, 30),
  519. ]
  520. self.assertEqual(
  521. existing_plsteam.exists(), self.env["crm.lead.scoring.frequency"],
  522. 'Frequencies of unlinked teams should be unlinked (cascade)')
  523. existing_noteam = self.env["crm.lead.scoring.frequency"].sudo().search([
  524. ('team_id', '=', False),
  525. ('variable', 'in', ['stage_id', 'country_id']),
  526. ])
  527. for frequency in existing_noteam:
  528. stat = next(item for item in final_noteam if item[0] == frequency.variable and item[1] == frequency.value)
  529. self.assertEqual(frequency.won_count, stat[2])
  530. self.assertEqual(frequency.lost_count, stat[3])
  531. self.assertEqual(len(existing_noteam), len(final_noteam))