[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:
@@ -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)
|
||||
|
||||
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user