diff --git a/addons/crm/models/crm_team.py b/addons/crm/models/crm_team.py index bd78e2f3453..448814fd6e6 100644 --- a/addons/crm/models/crm_team.py +++ b/addons/crm/models/crm_team.py @@ -263,7 +263,7 @@ class Team(models.Model): if not self.env.user.has_group('sales_team.group_sale_manager') and not self.env.user.has_group('base.group_system'): raise exceptions.UserError(_('Lead/Opportunities automatic assignment is limited to managers or administrators')) - _logger.info('### START Lead Assignment (%d teams, %d sales persons, %.2f work_days)' % (len(self), len(self.crm_team_member_ids), work_days)) + _logger.info('### START Lead Assignment (%d teams, %d sales persons, %.2f work_days)', len(self), len(self.crm_team_member_ids), work_days) teams_data = self._allocate_leads(work_days=work_days) _logger.info('### Team repartition done. Starting salesmen assignment.') members_data = self.crm_team_member_ids._assign_and_convert_leads(work_days=work_days) @@ -342,54 +342,45 @@ class Team(models.Model): No salesperson is assigned in this process. Its purpose is simply to allocate leads within teams. + This process allocates all available leads on teams weighted by their + maximum assignment by month that indicates their relative workload. + Heuristic of this method is the following: - - * first we randomize all teams; - * then for each team - - * find unassigned leads, aka leads being - - * without team, without user -> not assigned; - * not in a won stage, and not having False/0 (lost) or 100 (won) - probability) -> live leads; - * if set, a delay after creation can be applied (see BUNDLE_HOURS_DELAY) - parameter explanations here below; - - * keep only leads matching the team's assignment domain (empty means + * find unassigned leads for each team, aka leads being + * without team, without user -> not assigned; + * not in a won stage, and not having False/0 (lost) or 100 (won) + probability) -> live leads; + * if set, a delay after creation can be applied (see BUNDLE_HOURS_DELAY) + parameter explanations here below; + * matching the team's assignment domain (empty means everything); - * assign maximum BUNDLE_SIZE leads to the team, then move to the - next team. This is done to ensure every team will have leads - enough to fill its capacity based on its domain; - * when setting a team on leads, leads belonging to the current batch - are also merged. Purpose is to clean database and avoid assigning + + * assign a weight to each team based on their assignment_max that + indicates their relative workload; + + * pick a random team using a weighted random choice and find a lead + to assign: + + * remove already assigned leads from the available leads. If there + is not any lead spare to assign, remove team from active teams; + * pick the first lead and set the current team; + * when setting a team on leads, leads are also merged with their + duplicates. Purpose is to clean database and avoid assigning duplicates to same or different teams; + * add lead and its duplicates to already assigned leads; - * evaluate which teams still need to receive leads. This is based on - team maximum capacity. We consider a team should receive twice its - capacity as leads. That way members will receive leads and can pick - some leads in team unassigned pool of leads; + * pick another random team until their is no more leads to assign + to any team; - Note that leads are assigned in batch meaning a team could receive - leads that could better fit another team. However this heuristics is - based on hypothesis that team domains do not overlap. Indeed if a - company has several teams they will probably target separate market - segments: country-based, customer type or size, ... Having several - teams using same assignment domain could lead to less fairness in - assignment process but this should not be the target use case of this - heuristic. + This process ensure that teams having overlapping domains will all + receive leads as lead allocation is done one lead at a time. This + allocation will be proportional to their size (assignment of their + members). - Leads are allocated by batch. This can be configured using a config - parameter (see here below). Batch size depends on cron frequency, - lead pipeline size and members assignment maximum. Finding an optimal - heuristic for this parameter is not easy as it depends on internal - processes and organization. Higher batch size leads to better performances - when running automatic assignment. It can also give unfair results - if teams domain overlap or if pipeline is not big enough to fill all - teams capacity. - - :config int crm.assignment.bundle: optional config parameter allowing - to set size of lead batch (BUNDLE_SIZE) allocated to a team at each - iteration (50 by default based on experience); + :config int crm.assignment.bundle: deprecated + :config int crm.assignment.commit.bundle: optional config parameter allowing + to set size of lead batch to be committed together. By default 100 + which is a good trade-off between transaction time and speed :config int crm.assignment.delay: optional config parameter giving a delay before taking a lead into assignment process (BUNDLE_HOURS_DELAY) given in hours. Purpose if to allow other crons or automated actions @@ -414,85 +405,92 @@ class Team(models.Model): raise ValueError( _('Leads team allocation should be done for at least 0.2 or maximum 30 work days, not %.2f.', work_days) ) - # assignment_max is valid for "30 days" -> divide by requested work_days - # to have number of leads to assign - assign_ratio = work_days / 30.0 BUNDLE_HOURS_DELAY = int(self.env['ir.config_parameter'].sudo().get_param('crm.assignment.delay', default=0)) - BUNDLE_SIZE = int(self.env['ir.config_parameter'].sudo().get_param('crm.assignment.bundle', default=50)) + BUNDLE_COMMIT_SIZE = int(self.env['ir.config_parameter'].sudo().get_param('crm.assignment.commit.bundle', 100)) + auto_commit = not getattr(threading.currentThread(), 'testing', False) # leads max_create_dt = fields.Datetime.now() - datetime.timedelta(hours=BUNDLE_HOURS_DELAY) duplicates_lead_cache = dict() - # teams - team_done = self.env['crm.team'] - remaining_teams = self.env['crm.team'].browse(random.sample(self.ids, k=len(self.ids))) - # compute assign domain for each team before looping on them by bundle size - teams_domain = dict( - (team, literal_eval(team.assignment_domain or '[]')) - for team in remaining_teams - ) - # compute limit of leads to assign to each team: 2 times team capacity, based on given work_days - teams_limit = dict( - (team, 2 * team.assignment_max * assign_ratio) - for team in remaining_teams - ) + # teams data + teams_data, population, weights = dict(), list(), list() + for team in self: + if not team.assignment_max: + continue + + lead_domain = expression.AND([ + literal_eval(team.assignment_domain or '[]'), + [('create_date', '<', max_create_dt)], + ['&', ('team_id', '=', False), ('user_id', '=', False)], + ['|', ('stage_id', '=', False), ('stage_id.is_won', '=', False)] + ]) + + leads = self.env["crm.lead"].search(lead_domain) + # Fill duplicate cache: search for duplicate lead before the assignation + # avoid to flush during the search at every assignation + for lead in leads: + if lead not in duplicates_lead_cache: + duplicates_lead_cache[lead] = lead._get_lead_duplicates(email=lead.email_from) + + teams_data[team] = { + "team": team, + "leads": leads, + "assigned": set(), + "merged": set(), + "duplicates": set(), + } + population.append(team) + weights.append(team.assignment_max) + # assignment process data global_data = dict(assigned=set(), merged=set(), duplicates=set()) - teams_data = dict.fromkeys(remaining_teams, False) - for team in remaining_teams: - teams_data[team] = dict(assigned=set(), merged=set(), duplicates=set()) + leads_done_ids, lead_unlink_ids, counter = set(), set(), 0 + while population: + counter += 1 + team = random.choices(population, weights=weights, k=1)[0] - remaining_teams = remaining_teams.filtered('assignment_max') - while remaining_teams: - for team in remaining_teams: - lead_domain = expression.AND([ - teams_domain[team], - [('create_date', '<', max_create_dt)], - ['&', ('team_id', '=', False), ('user_id', '=', False)], - ['|', ('stage_id', '=', False), ('stage_id.is_won', '=', False)] - ]) - # assign only to reach asked team limit - remaining = teams_limit[team] - (len(teams_data[team]['assigned']) + len(teams_data[team]['merged'])) - lead_limit = min([BUNDLE_SIZE, remaining if remaining > 0 else 1]) - leads = self.env["crm.lead"].search(lead_domain, limit=lead_limit) + # filter remaining leads, remove team if no more leads for it + teams_data[team]["leads"] = teams_data[team]["leads"].filtered(lambda l: l.id not in leads_done_ids) + if not teams_data[team]["leads"]: + population_index = population.index(team) + population.pop(population_index) + weights.pop(population_index) + continue - # Fill duplicate cache: search for duplicate lead before the assignation - # avoid to flush during the search at every assignation - for lead in leads: - if lead not in duplicates_lead_cache: - duplicates_lead_cache[lead] = lead._get_lead_duplicates(email=lead.email_from) - - # assign + deduplicate and concatenate results in teams_data to keep some history - assign_res = team._allocate_leads_deduplicate(leads, duplicates_cache=duplicates_lead_cache) - _logger.info('Assigned %d leads among %d candidates to team %s' % (len(assign_res['assigned']) + len(assign_res['merged']), len(leads), team.id)) - _logger.info('\tLeads: direct assign %s / merge result %s / duplicates merged: %s' % ( - assign_res['assigned'], assign_res['merged'], assign_res['duplicates'] - )) - for key in ('assigned', 'merged', 'duplicates'): - global_data[key].update(assign_res[key]) - teams_data[team][key].update(assign_res[key]) + # assign + deduplicate and concatenate results in teams_data to keep some history + candidate_lead = teams_data[team]["leads"][0] + assign_res = team._allocate_leads_deduplicate(candidate_lead, duplicates_cache=duplicates_lead_cache) + for key in ('assigned', 'merged', 'duplicates'): + teams_data[team][key].update(assign_res[key]) + leads_done_ids.update(assign_res[key]) + global_data[key].update(assign_res[key]) + lead_unlink_ids.update(assign_res['duplicates']) + # auto-commit except in testing mode. As this process may be time consuming or we + # may encounter errors, already commit what is allocated to avoid endless cron loops. + if auto_commit and counter % BUNDLE_COMMIT_SIZE == 0: # unlink duplicates once - self.env['crm.lead'].browse(assign_res['duplicates']).unlink() + self.env['crm.lead'].browse(lead_unlink_ids).unlink() + lead_unlink_ids = set() + self._cr.commit() - # either no more lead matching domain, either asked capacity assigned - if len(leads) < lead_limit or (len(teams_data[team]['assigned']) + len(teams_data[team]['merged'])) >= teams_limit[team]: - team_done += team + # unlink duplicates once + self.env['crm.lead'].browse(lead_unlink_ids).unlink() - # auto-commit except in testing mode. As this process may be time consuming or we - # may encounter errors, already commit what is allocated to avoid endless cron loops. - auto_commit = not getattr(threading.currentThread(), 'testing', False) - if auto_commit: - self._cr.commit() - - remaining_team_ids = (remaining_teams - team_done).ids - remaining_teams = self.env['crm.team'].browse(random.sample(remaining_team_ids, k=len(remaining_team_ids))) + if auto_commit: + self._cr.commit() # some final log - _logger.info('## Assigned %s leads' % (len(global_data['assigned']) + len(global_data['merged']))) - + _logger.info('## Assigned %s leads', (len(global_data['assigned']) + len(global_data['merged']))) + for team, team_data in teams_data.items(): + _logger.info( + '## Assigned %s leads to team %s', + len(team_data['assigned']) + len(team_data['merged']), team.id) + _logger.info( + '\tLeads: direct assign %s / merge result %s / duplicates merged: %s', + team_data['assigned'], team_data['merged'], team_data['duplicates']) return teams_data def _allocate_leads_deduplicate(self, leads, duplicates_cache=None): @@ -538,11 +536,6 @@ class Team(models.Model): leads_dup_ids.update((lead_duplicates - merged).ids) leads_merged_ids.add(merged.id) - # auto-commit except in testing mode - auto_commit = not getattr(threading.currentThread(), 'testing', False) - if auto_commit: - self._cr.commit() - return { 'assigned': set(leads_assigned.ids), 'merged': leads_merged_ids, diff --git a/addons/crm/models/crm_team_member.py b/addons/crm/models/crm_team_member.py index 6da97a97a67..1c1995761e2 100644 --- a/addons/crm/models/crm_team_member.py +++ b/addons/crm/models/crm_team_member.py @@ -119,21 +119,15 @@ class Team(models.Model): raise ValueError( _('Leads team allocation should be done for at least 0.2 or maximum 30 work days, not %.2f.', work_days) ) - # assignment_max is valid for "30 days" -> divide by requested work_days - # to have number of leads to assign - assign_ratio = work_days / 30.0 members_data, population, weights = dict(), list(), list() - members = self.filtered(lambda member: not member.assignment_optout and member.assignment_max > member.lead_month_count) + members = self.filtered(lambda member: not member.assignment_optout and member.assignment_max > 0) if not members: return members_data # prepare a global lead count based on total leads to assign to salespersons lead_limit = sum( - min( - int(math.ceil(member.assignment_max * assign_ratio)), - (member.assignment_max - member.lead_month_count) - ) + member._get_assignment_quota(work_days=work_days) for member in members ) @@ -146,7 +140,7 @@ class Team(models.Model): leads = self.env["crm.lead"].search(lead_domain, order='probability DESC', limit=lead_limit) - to_assign = min(member.assignment_max - member.lead_month_count, round(member.assignment_max * assign_ratio)) + to_assign = member._get_assignment_quota(work_days=work_days) members_data[member.id] = { "team_member": member, "max": member.assignment_max, @@ -199,7 +193,24 @@ class Team(models.Model): (member_info["team_member"], {"assigned": member_info["assigned"]}) for member_id, member_info in members_data.items() ) - _logger.info('Assigned %s leads to %s salesmen' % (len(leads_done_ids), len(members))) + _logger.info('Assigned %s leads to %s salesmen', len(leads_done_ids), len(members)) for member, member_info in result_data.items(): - _logger.info('-> member %s: assigned %d leads (%s)' % (member.id, len(member_info["assigned"]), member_info["assigned"])) + _logger.info('-> member %s: assigned %d leads (%s)', member.id, len(member_info["assigned"]), member_info["assigned"]) return result_data + + def _get_assignment_quota(self, work_days=1): + """ Compute assignment quota based on work_days. This quota includes + a compensation to speedup getting to the lead average (``assignment_max``). + As this field is a counter for "30 days" -> divide by requested work + days in order to have base assign number then add compensation. Limit + to max capacity of team member. + + :param float work_days: see ``CrmTeam.action_assign_leads()``; + """ + assign_ratio = work_days / 30.0 + to_assign = self.assignment_max * assign_ratio + compensation = max(0, self.assignment_max - (self.lead_month_count + to_assign)) * 0.2 + return min( + self.assignment_max - self.lead_month_count, + round(to_assign + compensation) + ) diff --git a/addons/crm/tests/test_crm_lead_assignment.py b/addons/crm/tests/test_crm_lead_assignment.py index aca75789dc1..5d9431047dc 100644 --- a/addons/crm/tests/test_crm_lead_assignment.py +++ b/addons/crm/tests/test_crm_lead_assignment.py @@ -177,7 +177,7 @@ class TestLeadAssign(TestLeadAssignCommon): lead_type='lead', user_ids=[False], partner_ids=[self.contact_1.id, self.contact_2.id, False, False, False], - count=50 + count=200 ) # commit probability and related fields leads.flush() @@ -196,25 +196,20 @@ class TestLeadAssign(TestLeadAssignCommon): self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2) # teams assign - # due to duplicate management keeping master team, we may not ensure leads to be - # fulfilling their original team volume leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order leads_st1 = leads.filtered_domain([('team_id', '=', self.sales_team_1.id)]) leads_stc = leads.filtered_domain([('team_id', '=', self.sales_team_convert.id)]) - self.assertEqual(len(leads_st1), 10) # 2 * 2 * 75 / 30.0 - self.assertEqual(len(leads_stc), 12) # 2 * 2 * 90 / 30.0 + self.assertLessEqual(len(leads_st1), 128) + self.assertLessEqual(len(leads_stc), 96) + self.assertEqual(len(leads_st1) + len(leads_stc), len(leads)) # Make sure all lead are assigned # salespersons assign self.members.invalidate_cache(fnames=['lead_month_count']) - self.assertMemberAssign(self.sales_team_1_m1, 3) # 45 max on 2 days - self.assertMemberAssign(self.sales_team_1_m2, 1) # 15 max on 2 days - self.assertMemberAssign(self.sales_team_1_m3, 1) # 15 max on 2 days - self.assertMemberAssign(self.sales_team_convert_m1, 2) # 30 max on 15 - self.assertMemberAssign(self.sales_team_convert_m2, 4) # 60 max on 15 - - # run a second round to finish leads - with self.with_user('user_sales_manager'): - self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2) + 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) # teams assign: everything should be done due to duplicates leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order @@ -233,7 +228,7 @@ class TestLeadAssign(TestLeadAssignCommon): lead_type='lead', user_ids=[False], partner_ids=[False], - count=50 + count=100 ) # commit probability and related fields leads.flush() @@ -255,16 +250,17 @@ class TestLeadAssign(TestLeadAssignCommon): leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order leads_st1 = leads.filtered_domain([('team_id', '=', self.sales_team_1.id)]) leads_stc = leads.filtered_domain([('team_id', '=', self.sales_team_convert.id)]) - self.assertEqual(len(leads_st1), 10) # 2 * 2 * 75 / 30.0 - self.assertEqual(len(leads_stc), 12) # 2 * 2 * 90 / 30.0 + self.assertEqual(len(leads_st1) + len(leads_stc), 100) # 2 * 2 * 75 / 30.0 + self.assertLessEqual(len(leads_st1), 100) # 2 * 2 * 75 / 30.0 + self.assertLessEqual(len(leads_stc), 66) # 2 * 2 * 90 / 30.0 # salespersons assign self.members.invalidate_cache(fnames=['lead_month_count']) - self.assertMemberAssign(self.sales_team_1_m1, 3) # 45 max on 2 days - self.assertMemberAssign(self.sales_team_1_m2, 1) # 15 max on 2 days - self.assertMemberAssign(self.sales_team_1_m3, 1) # 15 max on 2 days - self.assertMemberAssign(self.sales_team_convert_m1, 2) # 30 max on 15 - self.assertMemberAssign(self.sales_team_convert_m2, 4) # 60 max on 15 + 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) @mute_logger('odoo.models.unlink') def test_assign_populated(self): @@ -331,6 +327,12 @@ class TestLeadAssign(TestLeadAssignCommon): with self.with_user('user_sales_manager'): self.env['crm.team'].browse(sales_teams.ids)._action_assign_leads(work_days=30) + # teams assign + leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) + self.assertEqual(leads.team_id, sales_teams) + self.assertEqual(leads.user_id, sales_teams.member_ids) + + # salespersons assign self.members.invalidate_cache(fnames=['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 @@ -341,6 +343,69 @@ class TestLeadAssign(TestLeadAssignCommon): self.assertMemberAssign(sales_team_3_m2, 60) # 60 max on one month self.assertMemberAssign(sales_team_3_m3, 15) # 15 max on one month + def test_assign_quota(self): + """ Test quota computation """ + self.assertInitialData() + + # 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: anyway 45 max available" + ) + self.assertEqual( + self.sales_team_1_m1._get_assignment_quota(work_days=60), + 45, + "Assignment quota: anyway 45 max available" + ) + + # create exiting leads for user_sales_leads (sales_team_1_m1) + existing_leads = self._create_leads_batch( + lead_type='lead', user_ids=[self.user_sales_leads.id], + probabilities=[10], + count=30) + self.assertEqual(existing_leads.team_id, self.sales_team_1, "Team should have lower sequence") + existing_leads.flush() + + self.sales_team_1_m1.invalidate_cache(fnames=['lead_month_count']) + self.assertEqual(self.sales_team_1_m1.lead_month_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.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), + 15, + "Assignment quota: anyway 15 max available (30 already assigned)" + ) + self.assertEqual( + self.sales_team_1_m1._get_assignment_quota(work_days=60), + 15, + "Assignment quota: anyway 15 max available (30 already assigned)" + ) + def test_assign_specific_won_lost(self): """ Test leads taken into account in assign process: won, lost, stage configuration. """ diff --git a/addons/crm/tests/test_performances.py b/addons/crm/tests/test_performances.py index 42de8145b7b..155adf7d612 100644 --- a/addons/crm/tests/test_performances.py +++ b/addons/crm/tests/test_performances.py @@ -34,7 +34,7 @@ class TestLeadAssignPerf(TestLeadAssignCommon): lead_type='lead', user_ids=[False], partner_ids=[self.contact_1.id, self.contact_2.id, False, False, False], - count=50 + count=200 ) # commit probability and related fields leads.flush() @@ -50,34 +50,24 @@ class TestLeadAssignPerf(TestLeadAssignCommon): leads.flush() with self.with_user('user_sales_manager'): - with self.assertQueryCount(user_sales_manager=483): # crm only: ?? + with self.assertQueryCount(user_sales_manager=1333): # crm only: ?? self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2) # teams assign - # due to duplicate management keeping master team, we may not ensure leads to be - # fulfilling their original team volume leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order leads_st1 = leads.filtered_domain([('team_id', '=', self.sales_team_1.id)]) leads_stc = leads.filtered_domain([('team_id', '=', self.sales_team_convert.id)]) - self.assertEqual(len(leads_st1), 10) # 2 * 2 * 75 / 30.0 - self.assertEqual(len(leads_stc), 12) # 2 * 2 * 90 / 30.0 + self.assertLessEqual(len(leads_st1), 128) + self.assertLessEqual(len(leads_stc), 96) + self.assertEqual(len(leads_st1) + len(leads_stc), len(leads)) # Make sure all lead are assigned # salespersons assign self.members.invalidate_cache(fnames=['lead_month_count']) - self.assertMemberAssign(self.sales_team_1_m1, 3) # 45 max on 2 days - self.assertMemberAssign(self.sales_team_1_m2, 1) # 15 max on 2 days - self.assertMemberAssign(self.sales_team_1_m3, 1) # 15 max on 2 days - self.assertMemberAssign(self.sales_team_convert_m1, 2) # 30 max on 15 - self.assertMemberAssign(self.sales_team_convert_m2, 4) # 60 max on 15 - - # run a second round to finish leads - with self.with_user('user_sales_manager'): - with self.assertQueryCount(user_sales_manager=137): # crm only: ?? - self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2) - - # teams assign: everything should be done due to duplicates - leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order - self.assertEqual(len(leads.filtered_domain([('team_id', '=', False)])), 0) + 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) @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): @@ -85,7 +75,7 @@ class TestLeadAssignPerf(TestLeadAssignCommon): lead_type='lead', user_ids=[False], partner_ids=[False], - count=50 + count=100 ) # commit probability and related fields leads.flush() @@ -99,23 +89,24 @@ class TestLeadAssignPerf(TestLeadAssignCommon): lead.probability = (idx + 1) * 10 * ((int(lead.priority) + 1) / 2) with self.with_user('user_sales_manager'): - with self.assertQueryCount(user_sales_manager=209): # crm only: 209 (seems reproducible) + with self.assertQueryCount(user_sales_manager=580): # crm only: 580 (seems reproducible) self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2) # teams assign leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order leads_st1 = leads.filtered_domain([('team_id', '=', self.sales_team_1.id)]) leads_stc = leads.filtered_domain([('team_id', '=', self.sales_team_convert.id)]) - self.assertEqual(len(leads_st1), 10) # 2 * 2 * 75 / 30.0 - self.assertEqual(len(leads_stc), 12) # 2 * 2 * 90 / 30.0 + self.assertEqual(len(leads_st1) + len(leads_stc), 100) # 2 * 2 * 75 / 30.0 + self.assertLessEqual(len(leads_st1), 100) # 2 * 2 * 75 / 30.0 + self.assertLessEqual(len(leads_stc), 66) # 2 * 2 * 90 / 30.0 # salespersons assign self.members.invalidate_cache(fnames=['lead_month_count']) - self.assertMemberAssign(self.sales_team_1_m1, 3) # 45 max on 2 days - self.assertMemberAssign(self.sales_team_1_m2, 1) # 15 max on 2 days - self.assertMemberAssign(self.sales_team_1_m3, 1) # 15 max on 2 days - self.assertMemberAssign(self.sales_team_convert_m1, 2) # 30 max on 15 - self.assertMemberAssign(self.sales_team_convert_m2, 4) # 60 max on 15 + 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) @mute_logger('odoo.models.unlink', 'odoo.addons.crm.models.crm_team', 'odoo.addons.crm.models.crm_team_member') def test_assign_perf_populated(self): @@ -180,9 +171,15 @@ class TestLeadAssignPerf(TestLeadAssignCommon): leads.flush() with self.with_user('user_sales_manager'): - with self.assertQueryCount(user_sales_manager=6293): # crm only: ?? + with self.assertQueryCount(user_sales_manager=5384): # crm only: ?? self.env['crm.team'].browse(sales_teams.ids)._action_assign_leads(work_days=30) + # teams assign + leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) + self.assertEqual(leads.team_id, sales_teams) + self.assertEqual(leads.user_id, sales_teams.member_ids) + + # salespersons assign self.members.invalidate_cache(fnames=['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