[REF] crm: limit allocation to teams when running automatic assign

GLOBAL PURPOSE

Ability to have salesmen belonging to several sales team is a core requirement
of CRM. It is therefore moved from website_crm_score to crm along with cleaning
and behavior improvement. Automatic lead assignment is also moved and cleaned.

SPECIFICATIONS

Currently when assignment runs all free leads are allocated to teams. They
are then assigned to members according to their capacity. This means that
no lead is kept free. If assignment is performed on a subset of teams this
means that other teams have no free lead anymore.

During assignment 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.

e.g. a team has a total of capacity of 90 leads / month among its 3 members.
When running a daily assignment process, an equivalent of twice this running
time is assigned to members (see previous commits). It leads to assignment
of (90 / 30) * 2 = 6 leads. Twice this count is allocated to the team aka
12 leads. Remaining leads will be allocated in next assignment run or are
available for manual assign.

LINKS

Task ID-2086889 (main task)
Task ID-2357969 (scoring migration task)
Community PR odoo/odoo#48422
Enterprise PR odoo/enterprise#499
Upgrade PR odoo/upgrade#996
This commit is contained in:
Thibault Delavallée
2021-01-21 12:22:09 +00:00
parent 8fd1066367
commit 6df2f0cfc0
2 changed files with 55 additions and 17 deletions
+27 -13
View File
@@ -306,10 +306,10 @@ class Team(models.Model):
are also merged. Purpose is to clean database and avoid assigning
duplicates to same or different teams;
* for all teams that still have capacity (aka: a search on unassigned
available leads with team domain still give results), do another
assignment round. Each round teams are randomized so that team order
is not always the same;
* 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;
Note that leads are assigned in batch meaning a team could receive
leads that could better fit another team. However this heuristics is
@@ -356,6 +356,9 @@ class Team(models.Model):
raise ValueError(
_('Leads team allocation should be done for at least 1 or maximum 30 work days, not %s.', 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))
@@ -365,14 +368,21 @@ class Team(models.Model):
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.fromkeys(remaining_teams, False)
for team in remaining_teams:
teams_domain[team] = safe_eval(team.assignment_domain or '[]', LEAD_ASSIGN_EVAL_CONTEXT)
teams_domain = dict(
(team, safe_eval(team.assignment_domain or '[]', LEAD_ASSIGN_EVAL_CONTEXT))
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
)
# assignment process data
teams_data = dict.fromkeys(remaining_teams, False)
for team in remaining_teams:
teams_data[team] = dict(assigned=set(), merged=set(), duplicates=set())
remaining_teams = remaining_teams.filtered('assignment_max')
while remaining_teams:
for team in remaining_teams:
lead_domain = expression.AND([
@@ -381,20 +391,24 @@ class Team(models.Model):
['&', ('team_id', '=', False), ('user_id', '=', False)],
['|', ('stage_id.is_won', '=', False), ('probability', 'not in', [False, 0, 100])]
])
leads = self.env["crm.lead"].search(lead_domain, limit=BUNDLE_SIZE)
if len(leads) < BUNDLE_SIZE:
team_done += team
# 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)
# assign + deduplicate and concatenate results in teams_data to keep some history
assign_res = team._allocate_leads_deduplicate(leads)
_logger.info('Assigned %s leads to team %s' % (len(leads), team.id))
_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'):
teams_data[team][key].update(assign_res[key])
# 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
# 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)
+28 -4
View File
@@ -115,9 +115,17 @@ class TestLeadAssign(TestLeadConvertCommon):
lead.probability = (idx + 1) * 10 * ((int(lead.priority) + 1) / 2)
with self.with_user('user_sales_manager'):
with self.assertQueryCount(user_sales_manager=505): # crm only: 505
with self.assertQueryCount(user_sales_manager=467): # crm only: 460
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
# 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
@@ -125,7 +133,15 @@ class TestLeadAssign(TestLeadConvertCommon):
self.assertMemberAssign(self.sales_team_convert_m1, 2) # 30 max on 15
self.assertMemberAssign(self.sales_team_convert_m2, 4) # 60 max on 15
# deduplicate should have removed all duplicated linked to contact_1 / contact_2
# 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)
# teams assign: everything should be done due to duplicates
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
self.assertTrue(len(leads.filtered_domain([('team_id', '=', False)])) == 0)
# deduplicate should have removed all duplicated linked to contact_1 and contact_2
new_assigned_leads_wpartner = self.env['crm.lead'].search([
('partner_id', 'in', (self.contact_1 | self.contact_2).ids),
('id', 'in', leads.ids)
@@ -149,9 +165,17 @@ class TestLeadAssign(TestLeadConvertCommon):
lead.probability = (idx + 1) * 10 * ((int(lead.priority) + 1) / 2)
with self.with_user('user_sales_manager'):
with self.assertQueryCount(user_sales_manager=319): # crm only: 319
with self.assertQueryCount(user_sales_manager=206): # crm only: 201
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
# 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
@@ -216,7 +240,7 @@ class TestLeadAssign(TestLeadConvertCommon):
lead.probability = (idx + 1) * 10 * ((int(lead.priority) + 1) / 2)
with self.with_user('user_sales_manager'):
with self.assertQueryCount(user_sales_manager=6199): # crm only: 6199
with self.assertQueryCount(user_sales_manager=6289): # crm only: 6165
self.env['crm.team'].browse(sales_teams.ids)._action_assign_leads(work_days=30)
self.members.invalidate_cache(fnames=['lead_month_count'])