[MERGE] crm(_*): move from onchange to editable stored fields

PURPOSE

Try to move from onchange / default_get to stored editable computed fields.
Behavior should be the same (computed or set by user), with support of
create / write / onchange field update without additional code.

USE EDITABLE STORED FIELDS

Update classic fields updated in some cases by onchange and/or default methods
by fields with store=True, readonly=False. It means their value comes either
from manual user input, either from trigger based computation.

Remove onchange and default_get when possible, leading to an unique computation
method and clearing fields definition.

Also clean some fields definition inconsistencies, notably required fields
that should instead be correctly computed or default that have no real meaning.

CLEAN CRM CONVERT WIZARDS CODE AND FIELDS USE

In this merge we also rewrite code located and/or used in convert or merge
wizards. Indeed it holds unnecessary variables, dictionaries manipulation
that hides the real business flow, badly named variables, unclear methods, ...
Logic of those wizards seem quite simple but is hidden behind a wall of
bloated code.

CLEAN CRM.TEAM MODEL

Clean statistics fields definition on crm.team. Future tasks will improve
the use of sales team in crm and first steps is to clean a bit models linked
to sales team.

LINKS
Task ID 2088565 (crm: from onchange to compute)
Community PR odoo/odoo#41955
Enterprise PR odoo/enterprise#7322
Upgrade PR odoo/upgrade#781

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2020-03-24 09:34:22 +01:00
committed by GitHub
21 changed files with 668 additions and 650 deletions
+235 -316
View File
@@ -45,8 +45,7 @@ CRM_LEAD_FIELDS_TO_MERGE = [
'date_action_last',
'email_from',
'email_cc',
'website',
'partner_name']
'website']
class Lead(models.Model):
@@ -61,16 +60,8 @@ class Lead(models.Model):
'phone.validation.mixin']
_primary_email = 'email_from'
def _default_team_id(self, user_id):
domain = [('use_leads', '=', True)] if self._context.get('default_type') == "lead" or self.type == 'lead' else [('use_opportunities', '=', True)]
return self.env['crm.team']._get_default_team_id(user_id=user_id, domain=domain)
def _default_stage_id(self):
team = self._default_team_id(user_id=self.env.uid)
return self._stage_find(team_id=team.id, domain=[('fold', '=', False)]).id
# Description
name = fields.Char('Opportunity', required=True, index=True)
name = fields.Char('Opportunity', index=True, required=True)
user_id = fields.Many2one('res.users', string='Salesperson', index=True, tracking=True, default=lambda self: self.env.user)
user_email = fields.Char('User Email', related='user_id.email', readonly=True)
user_login = fields.Char('User Login', related='user_id.login', readonly=True)
@@ -81,22 +72,23 @@ class Lead(models.Model):
type = fields.Selection([
('lead', 'Lead'), ('opportunity', 'Opportunity')],
index=True, required=True, tracking=15,
default=lambda self: 'lead' if self.env['res.users'].has_group('crm.group_use_lead') else 'opportunity',
help="Type is used to separate Leads and Opportunities")
default=lambda self: 'lead' if self.env['res.users'].has_group('crm.group_use_lead') else 'opportunity')
priority = fields.Selection(
crm_stage.AVAILABLE_PRIORITIES, string='Priority', index=True,
default=crm_stage.AVAILABLE_PRIORITIES[0][0])
team_id = fields.Many2one(
'crm.team', string='Sales Team', index=True, tracking=True,
default=lambda self: self._default_team_id(self.env.uid))
compute='_compute_team_id', copy=True, readonly=False, store=True)
stage_id = fields.Many2one(
'crm.stage', string='Stage', ondelete='restrict',
copy=False, group_expand='_read_group_stage_ids', index=True, tracking=True,
domain="['|', ('team_id', '=', False), ('team_id', '=', team_id)]",
default=lambda self: self._default_stage_id())
'crm.stage', string='Stage', index=True, tracking=True,
compute='_compute_stage_id', readonly=False, store=True,
copy=False, group_expand='_read_group_stage_ids', ondelete='restrict',
domain="['|', ('team_id', '=', False), ('team_id', '=', team_id)]")
kanban_state = fields.Selection([
('grey', 'No next activity planned'), ('red', 'Next activity late'), ('green', 'Next activity is planned')],
string='Kanban State', compute='_compute_kanban_state')
('grey', 'No next activity planned'),
('red', 'Next activity late'),
('green', 'Next activity is planned')], string='Kanban State',
compute='_compute_kanban_state')
tag_ids = fields.Many2many(
'crm.tag', 'crm_tag_rel', 'lead_id', 'tag_id', string='Tags',
help="Classify and analyze your lead/opportunity categories like: Training, Service")
@@ -108,26 +100,37 @@ class Lead(models.Model):
# Dates
date_closed = fields.Datetime('Closed Date', readonly=True, copy=False)
date_action_last = fields.Datetime('Last Action', readonly=True)
date_open = fields.Datetime('Assignment Date', readonly=True, default=fields.Datetime.now)
date_open = fields.Datetime(
'Assignment Date', compute='_compute_date_open', readonly=True, store=True)
day_open = fields.Float('Days to Assign', compute='_compute_day_open', store=True)
day_close = fields.Float('Days to Close', compute='_compute_day_close', store=True)
date_last_stage_update = fields.Datetime('Last Stage Update', index=True, default=fields.Datetime.now)
date_last_stage_update = fields.Datetime(
'Last Stage Update', compute='_compute_date_last_stage_update', index=True, readonly=True, store=True)
date_conversion = fields.Datetime('Conversion Date', readonly=True)
date_deadline = fields.Date('Expected Closing', help="Estimate of the date on which the opportunity will be won.")
# Customer / contact
partner_id = fields.Many2one(
'res.partner', string='Customer', tracking=10, index=True,
'res.partner', string='Customer', index=True, tracking=10,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
help="Linked partner (optional). Usually created when converting the lead. You can find a partner by its Name, TIN, Email or Internal Reference.")
partner_address_email = fields.Char('Partner Contact Email', related='partner_id.email', readonly=True)
partner_is_blacklisted = fields.Boolean('Partner is blacklisted', related='partner_id.is_blacklisted', readonly=True)
contact_name = fields.Char('Contact Name', tracking=30)
partner_name = fields.Char("Company Name", tracking=20, index=True, help='The name of the future partner company that will be created while converting the lead into opportunity')
function = fields.Char('Job Position')
title = fields.Many2one('res.partner.title', string='Title')
email_from = fields.Char('Email', index=True, tracking=40)
phone = fields.Char('Phone', tracking=50)
mobile = fields.Char('Mobile')
contact_name = fields.Char(
'Contact Name', tracking=30,
compute='_compute_partner_id_values', readonly=False, store=True)
partner_name = fields.Char(
'Company Name', tracking=20, index=True,
compute='_compute_partner_id_values', readonly=False, store=True,
help='The name of the future partner company that will be created while converting the lead into opportunity')
function = fields.Char('Job Position', compute='_compute_partner_id_values', readonly=False, store=True)
title = fields.Many2one('res.partner.title', string='Title',compute='_compute_partner_id_values', readonly=False, store=True)
email_from = fields.Char(
'Email', tracking=40, index=True,
compute='_compute_partner_id_values', readonly=False, store=True)
phone = fields.Char(
'Phone', tracking=50,
compute='_compute_partner_id_values', readonly=False, store=True)
mobile = fields.Char('Mobile', compute='_compute_partner_id_values', readonly=False, store=True)
phone_mobile_search = fields.Char('Phone/Mobile', store=False, search='_search_phone_mobile_search')
phone_state = fields.Selection([
('correct', 'Correct'),
@@ -135,18 +138,25 @@ class Lead(models.Model):
email_state = fields.Selection([
('correct', 'Correct'),
('incorrect', 'Incorrect')], string='Email Quality', compute="_compute_email_state", store=True)
website = fields.Char('Website', index=True, help="Website of the contact")
website = fields.Char('Website', index=True, help="Website of the contact", compute="_compute_partner_id_values", store=True, readonly=False)
lang_id = fields.Many2one('res.lang', string='Language')
# Address fields
street = fields.Char('Street')
street2 = fields.Char('Street2')
zip = fields.Char('Zip', change_default=True)
city = fields.Char('City')
state_id = fields.Many2one("res.country.state", string='State', domain="[('country_id', '=?', country_id)]")
country_id = fields.Many2one('res.country', string='Country')
street = fields.Char('Street', compute='_compute_partner_id_values', readonly=False, store=True)
street2 = fields.Char('Street2', compute='_compute_partner_id_values', readonly=False, store=True)
zip = fields.Char('Zip', change_default=True, compute='_compute_partner_id_values', readonly=False, store=True)
city = fields.Char('City', compute='_compute_partner_id_values', readonly=False, store=True)
state_id = fields.Many2one(
"res.country.state", string='State',
compute='_compute_partner_id_values', readonly=False, store=True,
domain="[('country_id', '=?', country_id)]")
country_id = fields.Many2one(
'res.country', string='Country',
compute='_compute_partner_id_values', readonly=False, store=True)
# Probability (Opportunity only)
probability = fields.Float('Probability', group_operator="avg", copy=False)
automated_probability = fields.Float('Automated Probability', readonly=True)
probability = fields.Float(
'Probability', group_operator="avg", copy=False,
compute='_compute_probabilities', readonly=False, store=True)
automated_probability = fields.Float('Automated Probability', compute='_compute_probabilities', readonly=True, store=True)
is_automated_probability = fields.Boolean('Is automated probability?', compute="_compute_is_automated_probability")
# External records
meeting_count = fields.Integer('# Meetings', compute='_compute_meeting_count')
@@ -170,12 +180,38 @@ class Lead(models.Model):
kanban_state = 'red'
lead.kanban_state = kanban_state
@api.depends('planned_revenue', 'probability')
def _compute_expected_revenue(self):
@api.depends('user_id', 'type')
def _compute_team_id(self):
""" When changing the user, also set a team_id or restrict team id
to the ones user_id is member of. """
for lead in self:
lead.expected_revenue = round((lead.planned_revenue or 0.0) * (lead.probability or 0) / 100.0, 2)
# setting user as void should not trigger a new team computation
if not lead.user_id:
continue
user = lead.user_id
if lead.team_id and user in lead.team_id.member_ids | lead.team_id.user_id:
continue
team_domain = [('use_leads', '=', True)] if lead.type == 'lead' else [('use_opportunities', '=', True)]
team = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=team_domain)
lead.team_id = team.id
@api.depends('date_open')
@api.depends('team_id', 'type')
def _compute_stage_id(self):
for lead in self:
if not lead.stage_id:
lead.stage_id = lead._stage_find(domain=[('fold', '=', False)]).id
@api.depends('user_id')
def _compute_date_open(self):
for lead in self:
lead.date_open = fields.Datetime.now() if lead.user_id else False
@api.depends('stage_id')
def _compute_date_last_stage_update(self):
for lead in self:
lead.date_last_stage_update = fields.Datetime.now()
@api.depends('create_date', 'date_open')
def _compute_day_open(self):
""" Compute difference between create date and open date """
leads = self.filtered(lambda l: l.date_open and l.create_date)
@@ -186,7 +222,7 @@ class Lead(models.Model):
date_open = fields.Datetime.from_string(lead.date_open)
lead.day_open = abs((date_open - date_create).days)
@api.depends('date_closed')
@api.depends('create_date', 'date_closed')
def _compute_day_close(self):
""" Compute difference between current date and log date """
leads = self.filtered(lambda l: l.date_closed and l.create_date)
@@ -197,13 +233,11 @@ class Lead(models.Model):
date_close = fields.Datetime.from_string(lead.date_closed)
lead.day_close = abs((date_close - date_create).days)
@api.depends('probability', 'automated_probability')
def _compute_is_automated_probability(self):
""" If probability and automated_probability are equal, the probability is considered as automatic.
when the probability is in automatic mode, the field probability will be aligned
when automated_probability is modified. """
@api.depends('partner_id')
def _compute_partner_id_values(self):
""" compute the new values when partner_id has changed """
for lead in self:
lead.is_automated_probability = tools.float_compare(lead.probability, lead.automated_probability, 2) == 0
lead.update(lead._preare_values_from_partner(lead.partner_id))
@api.depends('phone', 'country_id.code')
def _compute_phone_state(self):
@@ -230,6 +264,33 @@ class Lead(models.Model):
break
lead.email_state = email_state
@api.depends('probability', 'automated_probability')
def _compute_is_automated_probability(self):
""" If probability and automated_probability are equal probability computation
is considered as automatic, aka probability is sync with automated_probability """
for lead in self:
# creation mode: consider it as being not automated
if not lead.id and not lead._origin.id:
lead.is_automated_probability = False
else:
lead.is_automated_probability = tools.float_compare(lead.probability, lead.automated_probability, 2) == 0
@api.depends(lambda self: ['tag_ids', 'stage_id', 'team_id'] + self._pls_get_safe_fields())
def _compute_probabilities(self):
for lead in self:
was_automated = False
lead_probabilities = lead._pls_get_naive_bayes_probabilities()
if lead.id in lead_probabilities:
was_automated = lead.active and lead.is_automated_probability
lead.automated_probability = lead_probabilities[lead.id]
if was_automated:
lead.probability = lead.automated_probability
@api.depends('planned_revenue', 'probability')
def _compute_expected_revenue(self):
for lead in self:
lead.expected_revenue = round((lead.planned_revenue or 0.0) * (lead.probability or 0) / 100.0, 2)
def _compute_meeting_count(self):
meeting_data = self.env['calendar.event'].read_group([('opportunity_id', 'in', self.ids)], ['opportunity_id'], ['opportunity_id'])
mapped_data = {m['opportunity_id'][0]: m['opportunity_id_count'] for m in meeting_data}
@@ -261,121 +322,6 @@ class Lead(models.Model):
return [(0, '=', 1)]
return [('id', 'in', [r[0] for r in res])]
def _onchange_partner_id_values(self, partner_id):
""" returns the new values when partner_id has changed """
if partner_id:
partner = self.env['res.partner'].browse(partner_id)
partner_name = partner.parent_id.name
if not partner_name and partner.is_company:
partner_name = partner.name
return {
'partner_name': partner_name,
'contact_name': partner.name if not partner.is_company else False,
'title': partner.title.id,
'street': partner.street,
'street2': partner.street2,
'city': partner.city,
'state_id': partner.state_id.id,
'country_id': partner.country_id.id,
'email_from': partner.email,
'phone': partner.phone,
'mobile': partner.mobile,
'zip': partner.zip,
'function': partner.function,
'website': partner.website,
}
return {}
@api.onchange('partner_id')
def _onchange_partner_id(self):
values = self._onchange_partner_id_values(self.partner_id.id if self.partner_id else False)
self.update(values)
@api.model
def _onchange_user_values(self, user_id):
""" returns new values when user_id has changed """
if not user_id:
return {}
if user_id and self._context.get('team_id'):
team = self.env['crm.team'].browse(self._context['team_id'])
if user_id in team.member_ids.ids or user_id == team.user_id.id:
return {}
team_id = self._default_team_id(user_id)
return {'team_id': team_id}
@api.onchange('user_id')
def _onchange_user_id(self):
""" When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """
if self.user_id.sale_team_id:
values = self._onchange_user_values(self.user_id.id)
self.update(values)
def _onchange_compute_probability(self, optional_field_name=None):
"""Recompute probability on onchange methods of :
'stage_id', 'team_id', 'tag_ids'
'country_id', 'state_id', 'phone_state', 'email_state', 'source_id' """
if optional_field_name and optional_field_name not in self._pls_get_safe_fields():
return
lead_probabilities = self._pls_get_naive_bayes_probabilities()
if self.id in lead_probabilities:
self.automated_probability = lead_probabilities[self.id]
if self._origin.is_automated_probability:
self.probability = self.automated_probability
@api.onchange('stage_id')
def _onchange_stage_id(self):
self._onchange_compute_probability()
@api.onchange('source_id')
def _onchange_source_id(self):
self._onchange_compute_probability(optional_field_name='source_id')
@api.onchange('automated_probability')
def _onchange_automated_probability(self):
""" If was in auto mode before the change, align both fields so that it will stay in auto mode. """
if self._origin.is_automated_probability:
self.probability = self.automated_probability
@api.onchange('team_id')
def _onchange_team_id(self):
self._onchange_compute_probability()
@api.onchange('tag_ids')
def _onchange_tag_ids(self):
self._onchange_compute_probability()
@api.onchange('phone_state')
def _onchange_phone_state(self):
self._onchange_compute_probability(optional_field_name='phone_state')
@api.onchange('email_state')
def _onchange_email_state(self):
self._onchange_compute_probability(optional_field_name='email_state')
@api.constrains('user_id')
def _valid_team(self):
for lead in self:
if lead.user_id:
values = lead.with_context(team_id=lead.team_id.id)._onchange_user_values(lead.user_id.id)
if values:
lead.update(values)
@api.onchange('state_id')
def _onchange_state(self):
self._onchange_compute_probability(optional_field_name='state_id')
if self.state_id:
self.country_id = self.state_id.country_id.id
@api.onchange('country_id')
def _onchange_country_id(self):
self._onchange_compute_probability(optional_field_name='country_id')
@api.onchange('lang_id')
def _onchange_lang_id(self):
self._onchange_compute_probability(optional_field_name='lang_id')
@api.onchange('phone', 'country_id', 'company_id')
def _onchange_phone_validation(self):
if self.phone:
@@ -386,6 +332,30 @@ class Lead(models.Model):
if self.mobile:
self.mobile = self.phone_format(self.mobile)
def _preare_values_from_partner(self, partner):
""" Get a dictionary with values coming from customer information to
copy on a lead. Email_from and phone fields get the current lead
values to avoid being reset if customer has no value for them. """
partner_name = partner.parent_id.name
if not partner_name and partner.is_company:
partner_name = partner.name
return {
'partner_name': partner_name,
'contact_name': partner.name if not partner.is_company else False,
'title': partner.title.id,
'street': partner.street,
'street2': partner.street2,
'city': partner.city,
'state_id': partner.state_id.id,
'country_id': partner.country_id.id,
'email_from': partner.email or self.email_from,
'phone': partner.phone or self.phone,
'mobile': partner.mobile,
'zip': partner.zip,
'function': partner.function,
'website': partner.website,
}
# ------------------------------------------------------------
# ORM
# ------------------------------------------------------------
@@ -400,44 +370,23 @@ class Lead(models.Model):
@api.model
def create(self, vals):
# set up context used to find the lead's Sales Team which is needed
# to correctly set the default stage_id
context = dict(self._context or {})
if vals.get('type') and not self._context.get('default_type'):
context['default_type'] = vals.get('type')
if vals.get('team_id') and not self._context.get('default_team_id'):
context['default_team_id'] = vals.get('team_id')
if vals.get('user_id') and 'date_open' not in vals:
vals['date_open'] = fields.Datetime.now()
partner_id = vals.get('partner_id') or context.get('default_partner_id')
onchange_values = self._onchange_partner_id_values(partner_id)
onchange_values.update(vals) # we don't want to overwrite any existing key
vals = onchange_values
result = super(Lead, self.with_context(context)).create(vals)
lead = super(Lead, self).create(vals)
# Compute new probability for each lead separately
result._update_probability()
return result
lead._update_probability()
return lead
def write(self, vals):
# stage change:
if 'stage_id' in vals:
vals['date_last_stage_update'] = fields.Datetime.now()
stage_id = self.env['crm.stage'].browse(vals['stage_id'])
if stage_id.is_won:
vals.update({'probability': 100})
# Only write the 'date_open' if no salesperson was assigned.
if vals.get('user_id') and 'date_open' not in vals and not self.mapped('user_id'):
vals['date_open'] = fields.Datetime.now()
# stage change with new stage: update probability and date_closed
if vals.get('probability', 0) >= 100 or not vals.get('active', True):
vals['date_closed'] = fields.Datetime.now()
elif 'probability' in vals:
vals['date_closed'] = False
if vals.get('user_id') and 'date_open' not in vals:
vals['date_open'] = fields.Datetime.now()
write_result = super(Lead, self).write(vals)
# Compute new automated_probability (and, eventually, probability) for each lead separately
@@ -452,7 +401,7 @@ class Lead(models.Model):
if lead.id in lead_probabilities:
lead_proba = lead_probabilities[lead.id]
proba_vals = {'automated_probability': lead_proba}
if tools.float_compare(lead.probability, lead.automated_probability, 2) == 0:
if lead.active and lead.is_automated_probability:
proba_vals['probability'] = lead_proba
super(Lead, lead).write(proba_vals)
return
@@ -870,51 +819,18 @@ class Lead(models.Model):
return self.sorted(key=opps_key, reverse=reverse)
def get_duplicated_leads(self, partner_id, include_lost=False):
""" Search for opportunities that have the same partner and that arent done or cancelled
:param partner_id : partner to search
"""
self.ensure_one()
email = self.partner_id.email or self.email_from
return self._get_duplicated_leads_by_emails(partner_id, email, include_lost=include_lost)
@api.model
def _get_duplicated_leads_by_emails(self, partner_id, email, include_lost=False):
""" Search for opportunities that have the same partner and that arent done or cancelled """
if not email:
return self.env['crm.lead']
partner_match_domain = []
for email in set(email_split(email) + [email]):
partner_match_domain.append(('email_from', '=ilike', email))
if partner_id:
partner_match_domain.append(('partner_id', '=', partner_id))
partner_match_domain = ['|'] * (len(partner_match_domain) - 1) + partner_match_domain
if not partner_match_domain:
return self.env['crm.lead']
domain = partner_match_domain
if not include_lost:
domain += ['&', ('active', '=', True), ('probability', '<', 100)]
else:
domain += ['|', '&', ('type', '=', 'lead'), ('active', '=', True), ('type', '=', 'opportunity')]
return self.search(domain)
def _convert_opportunity_data(self, customer, team_id=False):
""" Extract the data from a lead to create the opportunity
:param customer : res.partner record
:param team_id : identifier of the Sales Team to determine the stage
"""
new_team_id = team_id if team_id else self.team_id.id
upd_values = {}
if customer:
upd_values.update(self._onchange_partner_id_values(customer and customer.id))
upd_values['email_from'] = upd_values['email_from'] if upd_values.get('email_from') else self.email_from
upd_values['phone'] = upd_values['phone'] if upd_values.get('phone') else self.phone
upd_values.update({
upd_values = {
'partner_id': customer.id if customer else False,
'type': 'opportunity',
'date_open': fields.Datetime.now(),
'date_conversion': fields.Datetime.now(),
})
}
if not self.stage_id:
stage = self._stage_find(team_id=new_team_id)
upd_values['stage_id'] = stage.id
@@ -931,11 +847,64 @@ class Lead(models.Model):
lead.write(vals)
if user_ids or team_id:
self.allocate_salesman(user_ids, team_id)
self.handle_salesmen_assignment(user_ids, team_id)
return True
def _create_lead_partner_data(self, partner_name, is_company=False, parent_id=False):
def _get_lead_duplicates(self, partner=None, email=None, include_lost=False):
""" Search for leads that seem duplicated based on partner / email.
:param partner : optional customer when searching duplicated
:param email: email (possibly formatted) to search
:param boolean include_lost: if True, search includes archived opportunities
(still only active leads are considered). If False, search for active
and not won leads and opportunities;
"""
if not email and not partner:
return self.env['crm.lead']
domain = []
for normalized_email in [tools.email_normalize(email) for email in tools.email_split(email)]:
domain.append(('email_normalized', '=', normalized_email))
if partner:
domain.append(('partner_id', '=', partner.id))
domain = ['|'] * (len(domain) - 1) + domain
if not domain:
return self.env['crm.lead']
if include_lost:
domain += ['|', ('type', '=', 'opportunity'), ('active', '=', True)]
else:
domain += ['&', ('active', '=', True), '|', ('probability', '=', False), ('probability', '<', 100)]
return self.with_context(active_test=False).search(domain)
def _create_customer(self):
""" Create a partner from lead data and link it to the lead.
:return: newly-created partner browse record
"""
Partner = self.env['res.partner']
contact_name = self.contact_name
if not contact_name:
contact_name = Partner._parse_partner_name(self.email_from)[0] if self.email_from else False
if self.partner_name:
partner_company = Partner.create(self._prepare_customer_values(self.partner_name, is_company=True))
elif self.partner_id:
partner_company = self.partner_id
else:
partner_company = None
if contact_name:
return Partner.create(self._prepare_customer_values(contact_name, is_company=False, parent_id=partner_company.id if partner_company else False))
if partner_company:
return partner_company
return Partner.create(self._prepare_customer_values(self.name, is_company=False))
def _prepare_customer_values(self, partner_name, is_company=False, parent_id=False):
""" Extract data from lead to create a partner.
:param name : furtur name of the partner
@@ -967,107 +936,59 @@ class Lead(models.Model):
'type': 'contact'
}
def _create_lead_partner(self):
""" Create a partner from lead data
:returns res.partner record
"""
Partner = self.env['res.partner']
contact_name = self.contact_name
if not contact_name:
contact_name = Partner._parse_partner_name(self.email_from)[0] if self.email_from else False
if self.partner_name:
partner_company = Partner.create(self._create_lead_partner_data(self.partner_name, is_company=True))
elif self.partner_id:
partner_company = self.partner_id
else:
partner_company = None
if contact_name:
return Partner.create(self._create_lead_partner_data(contact_name, is_company=False, parent_id=partner_company.id if partner_company else False))
if partner_company:
return partner_company
return Partner.create(self._create_lead_partner_data(self.name, is_company=False))
def _find_matching_partner(self):
""" Try to find a matching partner with available information on the Lead.
Like the customer's name, email, phone number, etc.
:return int partner_id if any, False otherwise
""" Try to find a matching partner with available information on the
lead, using notably customer's name, email, phone, ...
:return: partner browse record
"""
self.ensure_one()
partner = self.partner_id
# find the best matching partner
Partner = self.env['res.partner']
if self.partner_id: # a partner is set already
return self.partner_id.id
if not partner and self.email_from:
partner = self.env['res.partner'].search([('email', '=', self.email_from)], limit=1)
if self.email_from: # search through the existing partners based on the lead's email
return Partner.search([('email', '=', self.email_from)], limit=1).id
if not partner:
# search through the existing partners based on the lead's partner or contact name
# to be aligned with _create_customer, search on lead's name as last possibility
for customer_potential_name in [self[field_name] for field_name in ['partner_name', 'contact_name', 'name'] if self[field_name]]:
partner = self.env['res.partner'].search([('name', 'ilike', '%' + customer_potential_name + '%')], limit=1)
if partner:
break
search_criteria = False
if self.partner_name: # search through the existing partners based on the lead's partner or contact name
search_criteria = self.partner_name
elif self.contact_name:
search_criteria = self.contact_name
elif self.name: # to be aligned with _create_lead_partner, search on lead's name as last possibility
search_criteria = self.name
return partner
if search_criteria:
return Partner.search([('name', 'ilike', '%' + search_criteria + '%')], limit=1).id
def handle_partner_assignment(self, force_partner_id=False, create_missing=True):
""" Update customer (partner_id) of leads. Purpose is to set the same
partner on most leads; either through a newly created partner either
through a given partner_id.
return False
def handle_partner_assignation(self, action='create', partner_id=False):
""" Handle partner assignation during a lead conversion.
If action is 'create', create new partner with contact and assign lead to
new partner_id. Otherwise assign lead to the specified partner_id
TDE FIXME: docstring does not match code... code seems a bit random.
:param string action: what has to be done regarding partners (create it, assign an existing one, or nothing)
:param int partner_id: partner to assign if any
:return: dict(lead_id: partner_id)
:param int force_partner_id: if set, update all leads to that customer;
:param create_missing: for leads without customer, create a new one
based on lead information;
"""
partner_ids = {}
for lead in self:
if partner_id:
lead.partner_id = partner_id
if lead.partner_id:
partner_ids[lead.id] = lead.partner_id.id
continue
if action == 'create':
partner = lead._create_lead_partner()
partner_id = partner.id
partner.team_id = lead.team_id
partner_ids[lead.id] = partner_id
return partner_ids
if force_partner_id:
lead.partner_id = force_partner_id
if not lead.partner_id and create_missing:
partner = lead._create_customer()
lead.partner_id = partner.id
def allocate_salesman(self, user_ids=None, team_id=False):
def handle_salesmen_assignment(self, user_ids=None, team_id=False):
""" Assign salesmen and salesteam to a batch of leads. If there are more
leads than salesmen, these salesmen will be assigned in round-robin.
E.g.: 4 salesmen (S1, S2, S3, S4) for 6 leads (L1, L2, ... L6). They
will be assigned as followed: L1 - S1, L2 - S2, L3 - S3, L4 - S4,
L5 - S1, L6 - S2.
leads than salesmen, these salesmen will be assigned in round-robin. E.g.
4 salesmen (S1, S2, S3, S4) for 6 leads (L1, L2, ... L6) will assigned as
following: L1 - S1, L2 - S2, L3 - S3, L4 - S4, L5 - S1, L6 - S2.
:param list ids: leads/opportunities ids to process
:param list user_ids: salesmen to assign
:param int team_id: salesteam to assign
:return bool
:param list user_ids: salesmen to assign
:param int team_id: salesteam to assign
"""
index = 0
for lead in self:
value = {}
if team_id:
value['team_id'] = team_id
update_vals = {'team_id': team_id} if team_id else {}
for index, lead in enumerate(self):
if user_ids:
value['user_id'] = user_ids[index]
# Cycle through user_ids
index = (index + 1) % len(user_ids)
if value:
lead.write(value)
return True
update_vals['user_id'] = user_ids[index % len(user_ids)]
if update_vals:
lead.write(update_vals)
# ------------------------------------------------------------
# TOOLS
@@ -1200,8 +1121,6 @@ class Lead(models.Model):
'email_from': msg_dict.get('from'),
'partner_id': msg_dict.get('author_id', False),
}
if msg_dict.get('author_id'):
defaults.update(self._onchange_partner_id_values(msg_dict.get('author_id')))
if msg_dict.get('priority') in dict(crm_stage.AVAILABLE_PRIORITIES):
defaults['priority'] = msg_dict.get('priority')
defaults.update(custom_values)
+70 -35
View File
@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
import ast
import datetime
from odoo import api, fields, models, _
from odoo.tools.safe_eval import safe_eval
from odoo.exceptions import ValidationError
import ast
class Team(models.Model):
_name = 'crm.team'
@@ -17,30 +16,26 @@ class Team(models.Model):
use_leads = fields.Boolean('Leads', help="Check this box to filter and qualify incoming requests as leads before converting them into opportunities and assigning them to a salesperson.")
use_opportunities = fields.Boolean('Pipeline', default=True, help="Check this box to manage a presales process with opportunities.")
alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict", required=True, help="The email address associated with this channel. New emails received will automatically create new leads assigned to the channel.")
unassigned_leads_count = fields.Integer(
compute='_compute_unassigned_leads_count',
string='Unassigned Leads')
# statistics about leads / opportunities / both
lead_unassigned_count = fields.Integer(
string='# Unassigned Leads', compute='_compute_lead_unassigned_count')
lead_all_assigned_month_count = fields.Integer(
string='# Leads/Opps assigned this month', compute='_compute_lead_all_assigned_month_count',
help="Number of leads and opportunities assigned this last month.")
opportunities_count = fields.Integer(
compute='_compute_opportunities',
string='Number of open opportunities')
overdue_opportunities_count = fields.Integer(
compute='_compute_overdue_opportunities',
string='Number of overdue opportunities')
string='# Opportunities', compute='_compute_opportunities_data')
opportunities_amount = fields.Integer(
compute='_compute_opportunities',
string='Opportunities Revenues')
overdue_opportunities_amount = fields.Integer(
compute='_compute_overdue_opportunities',
string='Overdue Opportunities Revenues')
string='Opportunities Revenues', compute='_compute_opportunities_data')
opportunities_overdue_count = fields.Integer(
string='# Overdue Opportunities', compute='_compute_opportunities_overdue_data')
opportunities_overdue_amount = fields.Integer(
string='Overdue Opportunities Revenues', compute='_compute_opportunities_overdue_data',)
# alias: improve fields coming from _inherits, use inherited to avoid replacing them
alias_user_id = fields.Many2one(
'res.users', related='alias_id.alias_user_id', inherited=True,
domain=lambda self: [('groups_id', 'in', self.env.ref('sales_team.group_sale_salesman_all_leads').id)])
# Since we are in a _inherits case, this is not an override
# but a plain definition of a field
# So we need to reset the property related of that field
alias_user_id = fields.Many2one('res.users', related='alias_id.alias_user_id', inherited=True, domain=lambda self: [
('groups_id', 'in', self.env.ref('sales_team.group_sale_salesman_all_leads').id)])
def _compute_unassigned_leads_count(self):
def _compute_lead_unassigned_count(self):
leads_data = self.env['crm.lead'].read_group([
('team_id', 'in', self.ids),
('type', '=', 'lead'),
@@ -48,9 +43,20 @@ class Team(models.Model):
], ['team_id'], ['team_id'])
counts = {datum['team_id'][0]: datum['team_id_count'] for datum in leads_data}
for team in self:
team.unassigned_leads_count = counts.get(team.id, 0)
team.lead_unassigned_count = counts.get(team.id, 0)
def _compute_opportunities(self):
def _compute_lead_all_assigned_month_count(self):
limit_date = datetime.datetime.now() - datetime.timedelta(days=30)
leads_data = self.env['crm.lead'].read_group([
('team_id', 'in', self.ids),
('date_open', '>=', fields.Datetime.to_string(limit_date)),
('user_id', '!=', False),
], ['team_id'], ['team_id'])
counts = {datum['team_id'][0]: datum['team_id_count'] for datum in leads_data}
for team in self:
team.lead_all_assigned_month_count = counts.get(team.id, 0)
def _compute_opportunities_data(self):
opportunity_data = self.env['crm.lead'].search([
('team_id', 'in', self.ids),
('probability', '<', 100),
@@ -67,7 +73,7 @@ class Team(models.Model):
team.opportunities_count = counts.get(team.id, 0)
team.opportunities_amount = amounts.get(team.id, 0)
def _compute_overdue_opportunities(self):
def _compute_opportunities_overdue_data(self):
opportunity_data = self.env['crm.lead'].read_group([
('team_id', 'in', self.ids),
('probability', '<', 100),
@@ -77,14 +83,46 @@ class Team(models.Model):
counts = {datum['team_id'][0]: datum['team_id_count'] for datum in opportunity_data}
amounts = {datum['team_id'][0]: (datum['planned_revenue']) for datum in opportunity_data}
for team in self:
team.overdue_opportunities_count = counts.get(team.id, 0)
team.overdue_opportunities_amount = amounts.get(team.id, 0)
team.opportunities_overdue_count = counts.get(team.id, 0)
team.opportunities_overdue_amount = amounts.get(team.id, 0)
@api.onchange('use_leads', 'use_opportunities')
def _onchange_use_leads_opportunities(self):
if not self.use_leads and not self.use_opportunities:
self.alias_name = False
# ------------------------------------------------------------
# ORM
# ------------------------------------------------------------
@api.model
def create(self, vals):
alias_values = self._synchronize_alias(vals)
if alias_values:
vals.update(alias_values)
return super(Team, self).create(vals)
def write(self, vals):
result = super(Team, self).write(vals)
if 'use_leads' in vals or 'alias_defaults' in vals:
for team in self:
team.alias_id.write(team.get_alias_values())
if 'use_leads' in vals or 'use_opportunities' in vals:
alias_values = self._synchronize_alias(vals)
if alias_values:
self.write(alias_values)
return result
def _synchronize_alias(self, values):
use_leads = self.use_leads if self else values.get('use_leads', False)
use_opportunities = self.use_opportunities if self else values.get('use_opportunities', True)
if not use_leads and not use_opportunities:
return {'alias_name': False}
return {}
# ------------------------------------------------------------
# MESSAGING
# ------------------------------------------------------------
def get_alias_model_name(self, vals):
return 'crm.lead'
@@ -96,12 +134,9 @@ class Team(models.Model):
defaults['team_id'] = self.id
return values
def write(self, vals):
result = super(Team, self).write(vals)
if 'use_leads' in vals or 'alias_defaults' in vals:
for team in self:
team.alias_id.write(team.get_alias_values())
return result
# ------------------------------------------------------------
# ACTIONS
# ------------------------------------------------------------
#TODO JEM : refactor this stuff with xml action, proper customization,
@api.model
+14 -9
View File
@@ -2,14 +2,17 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from datetime import date
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
crm_alias_prefix = fields.Char('Default Alias Name for Leads')
generate_lead_from_alias = fields.Boolean('Manual Assignment of Emails', config_parameter='crm.generate_lead_from_alias')
crm_alias_prefix = fields.Char(
'Default Alias Name for Leads',
compute="_compute_crm_alias_prefix" , readonly=False, store=True)
generate_lead_from_alias = fields.Boolean(
'Manual Assignment of Emails', config_parameter='crm.generate_lead_from_alias',
compute="_compute_generate_lead_from_alias", readonly=False, store=True)
group_use_lead = fields.Boolean(string="Leads", implied_group='crm.group_use_lead')
module_crm_iap_lead = fields.Boolean("Generate new leads based on their country, industries, size, etc.")
module_crm_iap_lead_website = fields.Boolean("Create Leads/Opportunities from your website's traffic")
@@ -71,15 +74,17 @@ class ResConfigSettings(models.TransientModel):
if setting.predictive_lead_scoring_start_date:
setting.predictive_lead_scoring_start_date_str = fields.Date.to_string(setting.predictive_lead_scoring_start_date)
@api.onchange('group_use_lead')
def _onchange_group_use_lead(self):
@api.depends('group_use_lead')
def _compute_generate_lead_from_alias(self):
""" Reset alias / leads configuration if leads are not used """
if not self.group_use_lead:
self.generate_lead_from_alias = False
for setting in self:
setting.generate_lead_from_alias = False
@api.onchange('generate_lead_from_alias')
def _onchange_generate_lead_from_alias(self):
self.crm_alias_prefix = (self.crm_alias_prefix or 'info') if self.generate_lead_from_alias else False
@api.depends('generate_lead_from_alias')
def _compute_crm_alias_prefix(self):
for setting in self:
setting.crm_alias_prefix = (setting.crm_alias_prefix or 'info') if setting.generate_lead_from_alias else False
@api.model
def get_values(self):
-1
View File
@@ -5,7 +5,6 @@ from odoo import fields, models
class Users(models.Model):
_inherit = 'res.users'
target_sales_won = fields.Integer('Won in Opportunities Target')
+5 -2
View File
@@ -116,6 +116,10 @@ class TestCrmCommon(SavepointCase, MailCase):
'contact_name': 'Amy Wong',
'email_from': 'amy.wong@test.example.com',
})
# update lead_1: stage_id is not computed anymore by default for leads
cls.lead_1.write({
'stage_id': cls.stage_team1_1.id,
})
# create an history for new team
cls.lead_team_1_won = cls.env['crm.lead'].create({
@@ -303,6 +307,7 @@ class TestLeadConvertMassCommon(TestLeadConvertCommon):
'stage_id': False,
'partner_id': cls.contact_1.id,
})
cls.lead_w_partner.write({'stage_id': False})
cls.lead_w_partner_company = cls.env['crm.lead'].create({
'name': 'New1',
'type': 'lead',
@@ -338,6 +343,4 @@ class TestLeadConvertMassCommon(TestLeadConvertCommon):
'stage_id': cls.stage_team1_2.id,
'active': False,
})
for l in (cls.lead_w_partner | cls.lead_w_partner_company | cls.lead_w_contact | cls.lead_w_email | cls.lead_w_email_lost):
l._onchange_user_id()
(cls.lead_w_partner | cls.lead_w_partner_company | cls.lead_w_contact | cls.lead_w_email | cls.lead_w_email_lost).flush()
+3 -4
View File
@@ -78,7 +78,6 @@ class TestCRMLead(TestCrmCommon):
subtype_xmlid='mail.mt_comment')
self.assertEqual(message.author_id, self.user_sales_manager.partner_id)
new_partner_id = new_lead.handle_partner_assignation(action='create')[new_lead.id]
new_partner = self.env['res.partner'].with_user(self.user_sales_manager).browse(new_partner_id)
self.assertEqual(new_partner.email, 'unknown.sender@test.example.com')
self.assertEqual(new_partner.team_id, self.sales_team_1)
new_lead.handle_partner_assignment(create_missing=True)
self.assertEqual(new_lead.partner_id.email, 'unknown.sender@test.example.com')
self.assertEqual(new_lead.partner_id.team_id, self.sales_team_1)
+66 -53
View File
@@ -8,6 +8,10 @@ from odoo.tests.common import tagged, users
@tagged('lead_manage')
class TestLeadConvert(crm_common.TestLeadConvertCommon):
"""
TODO: created partner (handle assignation) has team of lead
TODO: create partner has user_id coming from wizard
"""
@classmethod
def setUpClass(cls):
@@ -15,6 +19,14 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
date = Datetime.from_string('2020-01-20 16:00:00')
cls.crm_lead_dt_mock.now.return_value = date
def test_initial_data(self):
""" Ensure initial data to avoid spaghetti test update afterwards """
self.assertFalse(self.lead_1.date_conversion)
self.assertEqual(self.lead_1.date_open, Datetime.from_string('2020-01-15 11:30:00'))
self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
self.assertEqual(self.lead_1.team_id, self.sales_team_1)
self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
@users('user_sales_manager')
def test_lead_convert_base(self):
""" Test base method ``convert_opportunity`` or crm.lead model """
@@ -91,24 +103,21 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
other_lead = self.lead_1.copy()
other_lead.write({'partner_id': self.contact_1.id})
# TDE FIXME: default_lead_id should be correctly supported by wizard -> currently completely broken
# convert = self.env['crm.lead2opportunity.partner'].with_context({
# 'default_lead_id': other_lead.id,
# }).create({})
# self.assertEqual(convert.lead_id, other_lead)
# self.assertEqual(convert.partner_id, self.contact_1)
# self.assertEqual(convert.action, 'exist')
convert = self.env['crm.lead2opportunity.partner'].with_context({
'default_lead_id': other_lead.id,
}).create({})
self.assertEqual(convert.lead_id, other_lead)
self.assertEqual(convert.partner_id, self.contact_1)
self.assertEqual(convert.action, 'exist')
convert = self.env['crm.lead2opportunity.partner'].with_context({
'default_lead_id': other_lead.id,
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
}).create({})
# TDE FIXME: active_id should be a fallback, not the only one way to work with a standard wizard
self.assertEqual(convert.lead_id, self.lead_1)
# self.assertEqual(convert.lead_id, other_lead)
# self.assertEqual(convert.partner_id, self.contact_1)
# self.assertEqual(convert.action, 'exist')
self.assertEqual(convert.lead_id, other_lead)
self.assertEqual(convert.partner_id, self.contact_1)
self.assertEqual(convert.action, 'exist')
@users('user_sales_manager')
def test_lead_convert_corner_cases_matching(self):
@@ -133,13 +142,6 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
@users('user_sales_manager')
def test_lead_convert_internals(self):
""" Test internals of convert wizard """
# ensure initial data to avoid spaghetti test update afterwards
self.assertFalse(self.lead_1.date_conversion)
self.assertEqual(self.lead_1.date_open, Datetime.from_string('2020-01-15 11:30:00'))
self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
self.assertEqual(self.lead_1.team_id, self.sales_team_1)
self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
@@ -155,7 +157,6 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
self.assertEqual(convert.action, 'create')
convert.write({'user_id': self.user_sales_salesman.id})
convert._onchange_user()
self.assertEqual(convert.user_id, self.user_sales_salesman)
self.assertEqual(convert.team_id, self.sales_team_convert)
@@ -226,7 +227,7 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
}).create({})
# test internals of convert wizard
self.assertEqual(convert.opportunity_ids, self.lead_1 | leads)
self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | leads)
self.assertEqual(convert.user_id, self.lead_1.user_id)
self.assertEqual(convert.team_id, self.lead_1.team_id)
self.assertFalse(convert.partner_id)
@@ -234,7 +235,6 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
self.assertEqual(convert.action, 'create')
convert.write({'user_id': self.user_sales_salesman.id})
convert._onchange_user()
self.assertEqual(convert.user_id, self.user_sales_salesman)
self.assertEqual(convert.team_id, self.sales_team_convert)
@@ -243,7 +243,7 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
@users('user_sales_manager')
def test_lead_merge_duplicates(self):
""" Test Lead._get_duplicated_leads_by_emails() """
""" Test Lead._get_lead_duplicates() """
# Check: partner / email fallbacks
self._create_duplicates(self.lead_1)
@@ -257,9 +257,8 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
'active_ids': self.lead_1.ids,
}).create({})
self.assertEqual(convert.partner_id, self.customer)
# TDE FIXME: should not give priority to partner email if it is void
# self.assertEqual(convert.opportunity_ids, self.lead_1 | lead_email_from | lead_email_normalized | lead_partner)
self.assertEqual(convert.opportunity_ids, self.env['crm.lead'])
# self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | self.lead_email_from | self.lead_email_normalized | self.lead_partner | self.opp_lost)
self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | self.lead_email_from | self.lead_partner | self.opp_lost)
# Check: partner fallbacks
self.lead_1.write({
@@ -273,13 +272,11 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
'active_ids': self.lead_1.ids,
}).create({})
self.assertEqual(convert.partner_id, self.customer)
# TDE FIXME: CHECKME: 2917b38f28d5c2d6c53c706e613da7b8e2ad7b52
# self.assertEqual(convert.opportunity_ids, self.lead_1 | lead_partner)
self.assertEqual(convert.opportunity_ids, self.env['crm.lead'])
self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | self.lead_partner)
@users('user_sales_manager')
def test_lead_merge_duplicates_flow(self):
""" Test Lead._get_duplicated_leads_by_emails() + merge with active_test """
""" Test Lead._get_lead_duplicates() + merge with active_test """
# Check: email formatting
self.lead_1.write({
@@ -294,9 +291,8 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
}).create({})
self.assertEqual(convert.partner_id, self.customer)
# TDE FIXME: should check for email_normalized -> lead_email_normalized not correctly found
# self.assertEqual(convert.opportunity_ids, self.lead_1 | lead_email_from | lead_email_normalized | lead_partner | opp_lost)
# TDE FIXME: add a active test context on opportunity_ids fields
self.assertEqual(convert.with_context(active_test=False).opportunity_ids, self.lead_1 | self.lead_email_from | self.lead_partner | self.opp_lost)
# self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | lead_email_from | lead_email_normalized | lead_partner | opp_lost)
self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | self.lead_email_from | self.lead_partner | self.opp_lost)
convert.action_apply()
self.assertEqual(
@@ -308,6 +304,34 @@ class TestLeadConvert(crm_common.TestLeadConvertCommon):
@tagged('lead_manage')
class TestLeadConvertBatch(crm_common.TestLeadConvertMassCommon):
def test_initial_data(self):
""" Ensure initial data to avoid spaghetti test update afterwards """
self.assertFalse(self.lead_1.date_conversion)
self.assertEqual(self.lead_1.date_open, Datetime.from_string('2020-01-15 11:30:00'))
self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
self.assertEqual(self.lead_1.team_id, self.sales_team_1)
self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
self.assertEqual(self.lead_w_partner.stage_id, self.env['crm.stage'])
self.assertEqual(self.lead_w_partner.user_id, self.user_sales_manager)
self.assertEqual(self.lead_w_partner.team_id, self.sales_team_1)
self.assertEqual(self.lead_w_partner_company.stage_id, self.stage_team1_1)
self.assertEqual(self.lead_w_partner_company.user_id, self.user_sales_manager)
self.assertEqual(self.lead_w_partner_company.team_id, self.sales_team_1)
self.assertEqual(self.lead_w_contact.stage_id, self.stage_gen_1)
self.assertEqual(self.lead_w_contact.user_id, self.user_sales_salesman)
self.assertEqual(self.lead_w_contact.team_id, self.sales_team_convert)
self.assertEqual(self.lead_w_email.stage_id, self.stage_gen_1)
self.assertEqual(self.lead_w_email.user_id, self.user_sales_salesman)
self.assertEqual(self.lead_w_email.team_id, self.sales_team_convert)
self.assertEqual(self.lead_w_email_lost.stage_id, self.stage_team1_2)
self.assertEqual(self.lead_w_email_lost.user_id, self.user_sales_leads)
self.assertEqual(self.lead_w_email_lost.team_id, self.sales_team_1)
@users('user_sales_manager')
def test_lead_convert_batch_internals(self):
""" Test internals of convert wizard, working in batch mode """
@@ -315,17 +339,8 @@ class TestLeadConvertBatch(crm_common.TestLeadConvertMassCommon):
self.crm_lead_dt_mock.now.return_value = date
lead_w_partner = self.lead_w_partner
self.assertEqual(lead_w_partner.user_id, self.user_sales_manager)
self.assertEqual(lead_w_partner.team_id, self.sales_team_1)
self.assertEqual(lead_w_partner.stage_id, self.env['crm.stage'])
lead_w_contact = self.lead_w_contact
self.assertEqual(lead_w_contact.user_id, self.user_sales_salesman)
self.assertEqual(lead_w_contact.team_id, self.sales_team_convert)
self.assertEqual(lead_w_contact.stage_id, self.stage_gen_1)
lead_w_email_lost = self.lead_w_email_lost
self.assertEqual(lead_w_email_lost.user_id, self.user_sales_leads)
self.assertEqual(lead_w_email_lost.team_id, self.sales_team_1)
self.assertEqual(lead_w_email_lost.stage_id, self.stage_team1_2)
lead_w_email_lost.action_set_lost()
self.assertEqual(lead_w_email_lost.active, False)
@@ -397,10 +412,8 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
}).create({
'deduplicate': False,
'user_id': self.user_sales_salesman.id,
'force_assignation': False,
'force_assignment': False,
})
mass_convert.onchange_action()
mass_convert._onchange_user()
# default values
self.assertEqual(mass_convert.name, 'convert')
@@ -411,7 +424,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
self.assertEqual(mass_convert.user_id, self.user_sales_salesman)
self.assertEqual(mass_convert.team_id, self.sales_team_convert)
mass_convert.mass_convert()
mass_convert.action_mass_convert()
for lead in self.lead_1 | self.lead_w_partner:
self.assertEqual(lead.type, 'opportunity')
if lead == self.lead_w_partner:
@@ -427,7 +440,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
mass_convert.write({
'user_ids': self.user_sales_salesman.ids,
})
mass_convert.mass_convert()
mass_convert.action_mass_convert()
self.assertEqual(self.lead_w_partner.user_id, self.user_sales_salesman)
self.assertEqual(self.lead_1.user_id, self.user_sales_leads) # existing value not forced
@@ -439,7 +452,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
# self.assertEqual(self.lead_w_email_lost.partner_id, self.env['res.partner'])
def test_mass_convert_deduplicate(self):
""" Test opportunity_ids fields having another behavior in mass convert
""" Test duplicated_lead_ids fields having another behavior in mass convert
because why not. Its use is: among leads under convert, store those with
duplicates if deduplicate is set to True. """
lead_1_dups = self._create_duplicates(self.lead_1, create_opp=False)
@@ -456,12 +469,12 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
}).create({
'deduplicate': True,
})
mass_convert._onchange_deduplicate()
self.assertEqual(mass_convert.action, 'each_exist_or_create')
self.assertEqual(mass_convert.name, 'convert')
self.assertEqual(mass_convert.opportunity_ids, self.lead_1 | self.lead_w_partner)
self.assertEqual(mass_convert.lead_tomerge_ids, self.leads)
self.assertEqual(mass_convert.duplicated_lead_ids, self.lead_1 | self.lead_w_partner)
mass_convert.mass_convert()
mass_convert.action_mass_convert()
self.assertEqual(
(lead_1_dups | lead_w_partner_dups).exists(),
@@ -484,11 +497,11 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
}).create({
'deduplicate': False,
'user_ids': self.assign_users.ids,
'force_assignation': True,
'force_assignment': True,
})
# TDE FIXME: what happens if we mix people from different sales team ? currently nothing, to check
mass_convert.mass_convert()
mass_convert.action_mass_convert()
for idx, lead in enumerate(self.leads - self.lead_w_email_lost):
self.assertEqual(lead.type, 'opportunity')
+33 -9
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests.common import TestLeadConvertMassCommon
from odoo.fields import Datetime
from odoo.tests.common import tagged, users
@@ -17,8 +18,9 @@ class TestLeadMerge(TestLeadConvertMassCommon):
cls.leads = cls.lead_1 + cls.lead_w_partner + cls.lead_w_contact + cls.lead_w_email + cls.lead_w_partner_company + cls.lead_w_email_lost
# reset some assigned users to test salesmen assign
(cls.lead_w_partner | cls.lead_w_email_lost).write({
'user_id': False
'user_id': False,
})
cls.lead_w_partner.write({'stage_id': False})
cls.lead_w_contact.write({'description': 'lead_w_contact'})
cls.lead_w_email.write({'description': 'lead_w_email'})
@@ -27,6 +29,34 @@ class TestLeadMerge(TestLeadConvertMassCommon):
cls.assign_users = cls.user_sales_manager + cls.user_sales_leads_convert + cls.user_sales_salesman
def test_initial_data(self):
""" Ensure initial data to avoid spaghetti test update afterwards """
self.assertFalse(self.lead_1.date_conversion)
self.assertEqual(self.lead_1.date_open, Datetime.from_string('2020-01-15 11:30:00'))
self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
self.assertEqual(self.lead_1.team_id, self.sales_team_1)
self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
self.assertEqual(self.lead_w_partner.stage_id, self.env['crm.stage'])
self.assertEqual(self.lead_w_partner.user_id, self.env['res.users'])
self.assertEqual(self.lead_w_partner.team_id, self.sales_team_1)
self.assertEqual(self.lead_w_partner_company.stage_id, self.stage_team1_1)
self.assertEqual(self.lead_w_partner_company.user_id, self.user_sales_manager)
self.assertEqual(self.lead_w_partner_company.team_id, self.sales_team_1)
self.assertEqual(self.lead_w_contact.stage_id, self.stage_gen_1)
self.assertEqual(self.lead_w_contact.user_id, self.user_sales_salesman)
self.assertEqual(self.lead_w_contact.team_id, self.sales_team_convert)
self.assertEqual(self.lead_w_email.stage_id, self.stage_gen_1)
self.assertEqual(self.lead_w_email.user_id, self.user_sales_salesman)
self.assertEqual(self.lead_w_email.team_id, self.sales_team_convert)
self.assertEqual(self.lead_w_email_lost.stage_id, self.stage_team1_2)
self.assertEqual(self.lead_w_email_lost.user_id, self.env['res.users'])
self.assertEqual(self.lead_w_email_lost.team_id, self.sales_team_1)
@users('user_sales_manager')
def test_lead_merge_internals(self):
""" Test internals of merge wizard. In this test leads are ordered as
@@ -37,9 +67,6 @@ class TestLeadMerge(TestLeadConvertMassCommon):
lead_w_partner --lead---seq=False
"""
# ensure initial data
self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
self.assertEqual(self.lead_1.team_id, self.sales_team_1)
self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
self.lead_w_partner_company.action_set_won() # won opps should be excluded
merge = self.env['crm.merge.opportunity'].with_context({
@@ -49,8 +76,6 @@ class TestLeadMerge(TestLeadConvertMassCommon):
}).create({
'user_id': self.user_sales_leads_convert.id,
})
# check user correctly triggered its sales team
merge._onchange_user()
self.assertEqual(merge.team_id, self.sales_team_convert)
# TDE FIXME: not sure the browse in default get of wizard intended to exlude lost, as it browse ids
@@ -96,7 +121,6 @@ class TestLeadMerge(TestLeadConvertMassCommon):
'user_id': False,
})
# TDE FIXME: see aa44700dccdc2618e0b8bc94252789264104047c -> no user, no team -> strange
merge._onchange_user()
merge.write({'team_id': self.sales_team_convert.id})
# TDE FIXME: not sure the browse in default get of wizard intended to exlude lost, as it browse ids
@@ -113,7 +137,7 @@ class TestLeadMerge(TestLeadConvertMassCommon):
# merged opportunity has same salesman (not updated in wizard)
self.assertEqual(merge_opportunity.user_id, self.user_sales_leads)
# TDE FIXME: as same uer_id is enforced, team is updated through onchange and therefore stage
# self.assertEqual(merge_opportunity.team_id, self.sales_team_convert)
self.assertEqual(merge_opportunity.team_id, self.sales_team_1)
self.assertEqual(merge_opportunity.team_id, self.sales_team_convert)
# self.assertEqual(merge_opportunity.team_id, self.sales_team_1)
# TDE FIXME: BUT team_id is computed after checking stage, based on wizard's team_id
self.assertEqual(merge_opportunity.stage_id, self.stage_team_convert_1)
+1 -1
View File
@@ -172,7 +172,7 @@
</group>
<group>
<field name="user_id" domain="[('share', '=', False)]"
context="{'team_id': team_id}"/>
context="{'default_sales_team_id': team_id}"/>
<field name="team_id" widget="selection"
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]"/>
<field name="type" invisible="1"/>
+7 -7
View File
@@ -191,11 +191,11 @@
</xpath>
<xpath expr="//t[@name='first_options']" position="after">
<div class="row" t-if="record.unassigned_leads_count.raw_value">
<div class="row" t-if="record.lead_unassigned_count.raw_value">
<div class="col-8">
<a name="%(crm_case_form_view_salesteams_lead)d" type="action" context="{'search_default_unassigned_leads': 1}">
<field name="unassigned_leads_count"/>
<t t-if="record.unassigned_leads_count.raw_value == 1">Unassigned Lead</t>
<field name="lead_unassigned_count"/>
<t t-if="record.lead_unassigned_count.raw_value == 1">Unassigned Lead</t>
<t t-else="">Unassigned Leads</t>
</a>
</div>
@@ -212,16 +212,16 @@
<field name="opportunities_amount" widget="monetary"/>
</div>
</div>
<div class="row" t-if="record.overdue_opportunities_count.raw_value">
<div class="row" t-if="record.opportunities_overdue_count.raw_value">
<div class="col-8">
<a name="%(crm_lead_action_team_overdue_opportunity)d" type="action">
<field name="overdue_opportunities_count"/>
<t t-if="record.overdue_opportunities_count.raw_value == 1">Overdue Opportunity</t>
<field name="opportunities_overdue_count"/>
<t t-if="record.opportunities_overdue_count.raw_value == 1">Overdue Opportunity</t>
<t t-else="">Overdue Opportunities</t>
</a>
</div>
<div class="col-4 text-right text-truncate">
<field name="overdue_opportunities_amount" widget="monetary"/>
<field name="opportunities_overdue_amount" widget="monetary"/>
</div>
</div>
</xpath>
+123 -112
View File
@@ -12,148 +12,159 @@ class Lead2OpportunityPartner(models.TransientModel):
@api.model
def default_get(self, fields):
""" Default get for name, opportunity_ids.
If there is an exisitng partner link to the lead, find all existing
opportunities links with this partner to merge all information together
"""
""" Allow support of active_id / active_model instead of jut default_lead_id
to ease window action definitions, and be backward compatible. """
result = super(Lead2OpportunityPartner, self).default_get(fields)
if self._context.get('active_id'):
tomerge = {int(self._context['active_id'])}
lead = self.env['crm.lead'].browse(self._context['active_id'])
result['lead_id'] = lead.id
partner_id = lead._find_matching_partner()
email = lead.partner_id.email if lead.partner_id else lead.email_from
tomerge.update(self.env['crm.lead']._get_duplicated_leads_by_emails(partner_id, email, include_lost=True).ids)
if 'action' in fields and not result.get('action'):
result['action'] = 'exist' if partner_id else 'create'
if 'partner_id' in fields:
result['partner_id'] = partner_id
if 'name' in fields:
result['name'] = 'merge' if len(tomerge) >= 2 else 'convert'
if 'opportunity_ids' in fields and len(tomerge) >= 2:
result['opportunity_ids'] = list(tomerge)
if lead.user_id:
result['user_id'] = lead.user_id.id
if lead.team_id:
result['team_id'] = lead.team_id.id
if not partner_id and not lead.contact_name:
result['action'] = 'nothing'
if not result.get('lead_id') and self.env.context.get('active_id'):
result['lead_id'] = self.env.context.get('active_id')
return result
name = fields.Selection([
('convert', 'Convert to opportunity'),
('merge', 'Merge with existing opportunities')
], 'Conversion Action', required=True)
], 'Conversion Action', compute='_compute_name', readonly=False, store=True)
action = fields.Selection([
('create', 'Create a new customer'),
('exist', 'Link to an existing customer'),
('nothing', 'Do not link to a customer')
], string='Related Customer', required=True)
lead_id = fields.Many2one('crm.lead', "Associated Lead")
opportunity_ids = fields.Many2many('crm.lead', string='Opportunities')
partner_id = fields.Many2one('res.partner', 'Customer')
user_id = fields.Many2one('res.users', 'Salesperson')
team_id = fields.Many2one('crm.team', 'Sales Team')
], string='Related Customer', compute='_compute_action', readonly=False, store=True)
lead_id = fields.Many2one('crm.lead', 'Associated Lead', required=True)
duplicated_lead_ids = fields.Many2many(
'crm.lead', string='Opportunities', context={'active_test': False},
compute='_compute_duplicated_lead_ids', readonly=False, store=True)
partner_id = fields.Many2one(
'res.partner', 'Customer',
compute='_compute_partner_id', readonly=False, store=True)
user_id = fields.Many2one(
'res.users', 'Salesperson',
compute='_compute_user_id', readonly=False, store=True)
team_id = fields.Many2one(
'crm.team', 'Sales Team',
compute='_compute_team_id', readonly=False, store=True)
force_assignment = fields.Boolean(
'Force assignment', default=True,
help='If checked, forces salesman to be updated on updated opportunities even if already set.')
@api.onchange('action')
def onchange_action(self):
if self.action == 'exist':
self.partner_id = self.lead_id._find_matching_partner()
else:
self.partner_id = False
@api.depends('duplicated_lead_ids')
def _compute_name(self):
for convert in self:
convert.name = 'merge' if convert.duplicated_lead_ids and len(convert.duplicated_lead_ids) >= 2 else 'convert'
@api.onchange('user_id')
def _onchange_user(self):
""" When changing the user, also set a team_id or restrict team id
to the ones user_id is member of.
"""
if self.user_id:
if self.team_id:
user_in_team = self.env['crm.team'].search_count([('id', '=', self.team_id.id), '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id)])
@api.depends('lead_id')
def _compute_action(self):
for convert in self:
if not convert.lead_id:
convert.action = 'nothing'
else:
user_in_team = False
if not user_in_team:
values = self.env['crm.lead']._onchange_user_values(self.user_id.id if self.user_id else False)
self.team_id = values.get('team_id', False)
partner = convert.lead_id._find_matching_partner()
if partner:
convert.action = 'exist'
elif convert.lead_id.contact_name:
convert.action = 'create'
else:
convert.action = 'nothing'
@api.depends('lead_id', 'partner_id')
def _compute_duplicated_lead_ids(self):
for convert in self:
if not convert.lead_id:
convert.duplicated_lead_ids = False
continue
convert.duplicated_lead_ids = self.env['crm.lead']._get_lead_duplicates(
convert.partner_id,
convert.lead_id.partner_id.email if convert.lead_id.partner_id.email else convert.lead_id.email_from,
include_lost=True).ids
@api.depends('action')
def _compute_partner_id(self):
for convert in self:
if convert.action == 'exist':
convert.partner_id = convert.lead_id._find_matching_partner()
else:
convert.partner_id = False
@api.depends('lead_id')
def _compute_user_id(self):
for convert in self:
convert.user_id = convert.lead_id.user_id if convert.lead_id.user_id else False
@api.depends('user_id')
def _compute_team_id(self):
""" When changing the user, also set a team_id or restrict team id
to the ones user_id is member of. """
for convert in self:
# setting user as void should not trigger a new team computation
if not convert.user_id:
continue
user = convert.user_id
if convert.team_id and user in convert.team_id.member_ids | convert.team_id.user_id:
continue
team_domain = []
team = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=team_domain)
convert.team_id = team.id
# NOTE JEM : is it the good place to test this ?
@api.model
def view_init(self, fields):
# JEM TDE FIXME: clean that brol
""" Check some preconditions before the wizard executes. """
for lead in self.env['crm.lead'].browse(self._context.get('active_ids', [])):
if lead.probability == 100:
raise UserError(_("Closed/Dead leads cannot be converted into opportunities."))
return False
def _convert_opportunity(self, vals):
def action_apply(self):
if self.name == 'merge':
result_opportunity = self._action_merge()
else:
result_opportunity = self._action_convert()
return result_opportunity.redirect_lead_opportunity_view()
def _action_merge(self):
to_merge = self.duplicated_lead_ids
result_opportunity = to_merge.merge_opportunity(auto_unlink=False)
if not result_opportunity.active:
result_opportunity.write({'active': True, 'activity_type_id': False, 'lost_reason': False})
if result_opportunity.type == "lead":
self._convert_and_allocate(result_opportunity, [self.user_id.id], team_id=self.team_id.id)
else:
if not result_opportunity.user_id or self.force_assignment:
result_opportunity.write({
'user_id': self.user_id.id,
'team_id': self.team_id.id,
})
(to_merge - result_opportunity).unlink()
return result_opportunity
def _action_convert(self):
""" """
result_opportunities = self.env['crm.lead'].browse(self._context.get('active_ids', []))
self._convert_and_allocate(result_opportunities, [self.user_id.id], team_id=self.team_id.id)
return result_opportunities[0]
def _convert_and_allocate(self, leads, user_ids, team_id=False):
self.ensure_one()
res = False
leads = self.env['crm.lead'].browse(vals.get('lead_ids'))
for lead in leads:
self_def_user = self.with_context(default_user_id=self.user_id.id)
if lead.active and self.action != 'nothing':
self._convert_handle_partner(
lead, self.action, self.partner_id.id or lead.partner_id.id)
partner_id = False
if self.action != 'nothing':
partner_id = self_def_user._create_partner(
lead.id, self.action, vals.get('partner_id') or lead.partner_id.id)
res = lead.convert_opportunity(partner_id, [], False)
user_ids = vals.get('user_ids')
lead.convert_opportunity(lead.partner_id.id, [], False)
leads_to_allocate = leads
if self._context.get('no_force_assignation'):
if not self.force_assignment:
leads_to_allocate = leads_to_allocate.filtered(lambda lead: not lead.user_id)
if user_ids:
leads_to_allocate.allocate_salesman(user_ids, team_id=(vals.get('team_id')))
leads_to_allocate.handle_salesmen_assignment(user_ids, team_id=team_id)
return res
def action_apply(self):
""" Convert lead to opportunity or merge lead and opportunity and open
the freshly created opportunity view.
"""
self.ensure_one()
values = {
'team_id': self.team_id.id,
}
if self.partner_id:
values['partner_id'] = self.partner_id.id
if self.name == 'merge':
leads = self.with_context(active_test=False).opportunity_ids.merge_opportunity()
if not leads.active:
leads.write({'active': True, 'activity_type_id': False, 'lost_reason': False})
if leads.type == "lead":
values.update({'lead_ids': leads.ids, 'user_ids': [self.user_id.id]})
self.with_context(active_ids=leads.ids)._convert_opportunity(values)
elif not self._context.get('no_force_assignation') or not leads.user_id:
values['user_id'] = self.user_id.id
leads.write(values)
else:
leads = self.env['crm.lead'].browse(self._context.get('active_ids', []))
values.update({'lead_ids': leads.ids, 'user_ids': [self.user_id.id]})
self._convert_opportunity(values)
return leads[0].redirect_lead_opportunity_view()
def _create_partner(self, lead_id, action, partner_id):
""" Create partner based on action.
:return dict: dictionary organized as followed: {lead_id: partner_assigned_id}
"""
#TODO this method in only called by Lead2OpportunityPartner
#wizard and would probably diserve to be refactored or at least
#moved to a better place
if action == 'each_exist_or_create':
partner_id = self.env['crm.lead'].browse(lead_id)._find_matching_partner()
action = 'create'
result = self.env['crm.lead'].browse(lead_id).handle_partner_assignation(action, partner_id)
return result.get(lead_id)
def _convert_handle_partner(self, lead, action, partner_id):
# used to propagate user_id (salesman) on created partners during conversion
lead.with_context(default_user_id=self.user_id.id).handle_partner_assignment(
force_partner_id=partner_id,
create_missing=(action == 'create')
)
@@ -5,7 +5,6 @@ from odoo import api, fields, models
class Lead2OpportunityMassConvert(models.TransientModel):
_name = 'crm.lead2opportunity.partner.mass'
_description = 'Convert Lead to Opportunity (in mass)'
_inherit = 'crm.lead2opportunity.partner'
@@ -13,70 +12,87 @@ class Lead2OpportunityMassConvert(models.TransientModel):
@api.model
def default_get(self, fields):
res = super(Lead2OpportunityMassConvert, self).default_get(fields)
if 'partner_id' in fields: # avoid forcing the partner of the first lead as default
res['partner_id'] = False
if 'action' in fields:
res['action'] = 'each_exist_or_create'
if 'name' in fields:
res['name'] = 'convert'
if 'opportunity_ids' in fields:
res['opportunity_ids'] = False
if 'lead_tomerge_ids' in fields and not res.get('lead_tomerge_ids'):
res['lead_tomerge_ids'] = self.env.context.get('active_ids', [])
return res
lead_id = fields.Many2one(required=False)
lead_tomerge_ids = fields.Many2many(
'crm.lead', 'crm_convert_lead_mass_lead_rel',
string='Active Leads', context={'active_test': False})
user_ids = fields.Many2many('res.users', string='Salesmen')
team_id = fields.Many2one('crm.team', 'Sales Team', index=True)
deduplicate = fields.Boolean('Apply deduplication', default=True, help='Merge with existing leads/opportunities of each partner')
action = fields.Selection(selection_add=[
('each_exist_or_create', 'Use existing partner or create'),
], string='Related Customer', required=True)
force_assignation = fields.Boolean('Force assignment', help='If unchecked, this will leave the salesman of duplicated opportunities')
], string='Related Customer')
force_assignment = fields.Boolean(default=False)
@api.onchange('action')
def _onchange_action(self):
if self.action != 'exist':
self.partner_id = False
@api.depends('duplicated_lead_ids')
def _compute_name(self):
for convert in self:
convert.name = 'convert'
@api.onchange('deduplicate')
def _onchange_deduplicate(self):
active_leads = self.env['crm.lead'].browse(self._context['active_ids'])
partner_ids = [(lead.partner_id.id, lead.partner_id and lead.partner_id.email or lead.email_from) for lead in active_leads]
partners_duplicated_leads = {}
for partner_id, email in partner_ids:
duplicated_leads = self.env['crm.lead']._get_duplicated_leads_by_emails(partner_id, email, include_lost=False)
if len(duplicated_leads) > 1:
partners_duplicated_leads.setdefault((partner_id, email), []).extend(duplicated_leads)
@api.depends('lead_tomerge_ids')
def _compute_action(self):
for convert in self:
convert.action = 'each_exist_or_create'
leads_with_duplicates = []
for lead in active_leads:
lead_tuple = (lead.partner_id.id, lead.partner_id.email if lead.partner_id else lead.email_from)
if len(partners_duplicated_leads.get(lead_tuple, [])) > 1:
leads_with_duplicates.append(lead.id)
@api.depends('lead_tomerge_ids')
def _compute_partner_id(self):
for convert in self:
convert.partner_id = False
self.opportunity_ids = self.env['crm.lead'].browse(leads_with_duplicates)
@api.depends('user_ids')
def _compute_team_id(self):
""" When changing the user, also set a team_id or restrict team id
to the ones user_id is member of. """
for convert in self:
# setting user as void should not trigger a new team computation
if not convert.user_id and not convert.user_ids and convert.team_id:
continue
user = convert.user_id or convert.user_ids and convert.user_ids[0] or self.env.user
if convert.team_id and user in convert.team_id.member_ids | convert.team_id.user_id:
continue
team_domain = []
team = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=team_domain)
convert.team_id = team.id
def _convert_opportunity(self, vals):
@api.depends('lead_tomerge_ids')
def _compute_duplicated_lead_ids(self):
for convert in self:
duplicated = self.env['crm.lead']
for lead in convert.lead_tomerge_ids:
duplicated_leads = self.env['crm.lead']._get_lead_duplicates(
partner=lead.partner_id,
email=lead.partner_id and lead.partner_id.email or lead.email_from,
include_lost=False)
if len(duplicated_leads) > 1:
duplicated += lead
convert.duplicated_lead_ids = duplicated.ids
def _convert_and_allocate(self, leads, user_ids, team_id=False):
""" When "massively" (more than one at a time) converting leads to
opportunities, check the salesteam_id and salesmen_ids and update
the values before calling super.
opportunities, check the salesteam_id and salesmen_ids and update
the values before calling super.
"""
self.ensure_one()
salesteam_id = self.team_id.id if self.team_id else False
salesmen_ids = []
if self.user_ids:
salesmen_ids = self.user_ids.ids
vals.update({'user_ids': salesmen_ids, 'team_id': salesteam_id})
return super(Lead2OpportunityMassConvert, self)._convert_opportunity(vals)
return super(Lead2OpportunityMassConvert, self)._convert_and_allocate(leads, salesmen_ids, team_id=team_id)
def mass_convert(self):
def action_mass_convert(self):
self.ensure_one()
if self.name == 'convert' and self.deduplicate:
merged_lead_ids = set()
remaining_lead_ids = set()
lead_selected = self._context.get('active_ids', [])
for lead_id in lead_selected:
if lead_id not in merged_lead_ids:
lead = self.env['crm.lead'].browse(lead_id)
duplicated_leads = self.env['crm.lead']._get_duplicated_leads_by_emails(lead.partner_id.id, lead.partner_id.email if lead.partner_id else lead.email_from, include_lost=False)
for lead in self.lead_tomerge_ids:
if lead not in merged_lead_ids:
duplicated_leads = self.env['crm.lead']._get_lead_duplicates(
partner=lead.partner_id,
email=lead.partner_id.email or lead.email_from,
include_lost=False
)
if len(duplicated_leads) > 1:
lead = duplicated_leads.merge_opportunity()
merged_lead_ids.update(duplicated_leads.ids)
@@ -85,5 +101,10 @@ class Lead2OpportunityMassConvert(models.TransientModel):
active_ids = (active_ids - merged_lead_ids) | remaining_lead_ids
self = self.with_context(active_ids=list(active_ids)) # only update active_ids when there are set
no_force_assignation = self._context.get('no_force_assignation', not self.force_assignation)
return self.with_context(no_force_assignation=no_force_assignation).action_apply()
return self.action_apply()
def _convert_handle_partner(self, lead, action, partner_id):
if self.action == 'each_exist_or_create':
partner_id = lead._find_matching_partner().id
action = 'create'
return super(Lead2OpportunityMassConvert, self)._convert_handle_partner(lead, action, partner_id)
@@ -5,6 +5,7 @@
<field name="model">crm.lead2opportunity.partner.mass</field>
<field name="arch" type="xml">
<form string="Convert to Opportunity">
<field name="lead_tomerge_ids" invisible="1"/>
<separator string="Conversion Options"/>
<group>
<field name="name" class="oe_inline" widget="radio"/>
@@ -13,11 +14,11 @@
<group string="Assign these opportunities to">
<field name="team_id" kanban_view_ref="%(sales_team.crm_team_view_kanban)s"/>
<field name="user_ids" widget="many2many_tags" domain="[('share', '=', False)]"/>
<field name="force_assignation"/>
<field name="force_assignment"/>
</group>
<label for="opportunity_ids" string="Leads with existing duplicates (for information)" help="Leads that you selected that have duplicates. If the list is empty, it means that no duplicates were found" attrs="{'invisible': [('deduplicate', '=', False)]}"/>
<label for="duplicated_lead_ids" string="Leads with existing duplicates (for information)" help="Leads that you selected that have duplicates. If the list is empty, it means that no duplicates were found" attrs="{'invisible': [('deduplicate', '=', False)]}"/>
<group attrs="{'invisible': [('deduplicate', '=', False)]}">
<field name="opportunity_ids" colspan="4" nolabel="1" readonly="1">
<field name="duplicated_lead_ids" colspan="4" nolabel="1" readonly="1">
<tree create="false" delete="false">
<field name="create_date" widget="date"/>
<field name="name"/>
@@ -42,7 +43,7 @@
</group>
</group>
<footer>
<button name="mass_convert" string="Convert to Opportunities" type="object" class="btn-primary"/>
<button string="Convert to Opportunities" name="action_mass_convert" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
@@ -55,7 +56,7 @@
res_model="crm.lead2opportunity.partner.mass"
binding_model="crm.lead" binding_views="list"
view_mode="form" target="new"
context="{'mass_convert' : True}"
context="{}"
view_id="view_crm_lead2opportunity_partner_mass"
/>
</odoo>
@@ -13,7 +13,7 @@
<field name="team_id" widget="selection"/>
</group>
<group string="Opportunities" attrs="{'invisible': [('name', '!=', 'merge')]}">
<field name="opportunity_ids" nolabel="1">
<field name="duplicated_lead_ids" nolabel="1">
<tree>
<field name="create_date" widget="date"/>
<field name="name"/>
+12 -11
View File
@@ -34,22 +34,23 @@ class MergeOpportunity(models.TransientModel):
opportunity_ids = fields.Many2many('crm.lead', 'merge_opportunity_rel', 'merge_id', 'opportunity_id', string='Leads/Opportunities')
user_id = fields.Many2one('res.users', 'Salesperson', index=True)
team_id = fields.Many2one('crm.team', 'Sales Team', index=True)
team_id = fields.Many2one(
'crm.team', 'Sales Team', index=True,
compute='_compute_team_id', readonly=False, store=True)
def action_merge(self):
self.ensure_one()
merge_opportunity = self.opportunity_ids.merge_opportunity(self.user_id.id, self.team_id.id)
return merge_opportunity.redirect_lead_opportunity_view()
@api.onchange('user_id')
def _onchange_user(self):
@api.depends('user_id')
def _compute_team_id(self):
""" When changing the user, also set a team_id or restrict team id
to the ones user_id is member of. """
team_id = False
if self.user_id:
user_in_team = False
if self.team_id:
user_in_team = self.env['crm.team'].search_count([('id', '=', self.team_id.id), '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id)])
if not user_in_team:
team_id = self.env['crm.team'].search(['|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id)], limit=1)
self.team_id = team_id
for wizard in self:
if wizard.user_id:
user_in_team = False
if wizard.team_id:
user_in_team = wizard.env['crm.team'].search_count([('id', '=', wizard.team_id.id), '|', ('user_id', '=', wizard.user_id.id), ('member_ids', '=', wizard.user_id.id)])
if not user_in_team:
wizard.team_id = wizard.env['crm.team'].search(['|', ('user_id', '=', wizard.user_id.id), ('member_ids', '=', wizard.user_id.id)], limit=1)
+1 -3
View File
@@ -31,7 +31,7 @@ class MailChannel(models.Model):
for message in self.channel_message_ids.sorted('id')
)
utm_source = self.env.ref('crm_livechat.utm_source_livechat', raise_if_not_found=False)
lead = self.env['crm.lead'].create({
return self.env['crm.lead'].create({
'name': html2plaintext(key[5:]),
'partner_id': channel_partners.partner_id.id,
'user_id': None,
@@ -40,5 +40,3 @@ class MailChannel(models.Model):
'referred': partner.name,
'source_id': utm_source and utm_source.id,
})
lead._onchange_partner_id()
return lead
@@ -24,7 +24,7 @@ class Opportunity2Quotation(models.TransientModel):
lead = self.env['crm.lead'].browse(self._context['active_id'])
if lead:
result['lead_id'] = lead.id
partner_id = result.get('partner_id') or lead._find_matching_partner()
partner_id = result.get('partner_id') or lead._find_matching_partner().id
if 'action' in fields and not result.get('action'):
result['action'] = 'exist' if partner_id else 'create'
if 'partner_id' in fields and not result.get('partner_id'):
@@ -46,16 +46,5 @@ class Opportunity2Quotation(models.TransientModel):
"""
self.ensure_one()
if self.action != 'nothing':
self.lead_id.write({
'partner_id': self.partner_id.id if self.action == 'exist' else self._create_partner()
})
self.lead_id._onchange_partner_id()
self.lead_id.handle_partner_assignment(force_partner_id=self.partner_id.id, create_missing=(self.action == 'create'))
return self.lead_id.action_new_quotation()
def _create_partner(self):
""" Create partner based on action.
:return int: created res.partner id
"""
self.ensure_one()
result = self.lead_id.handle_partner_assignation(action='create')
return result.get(self.lead_id.id)
+2 -4
View File
@@ -51,10 +51,8 @@ class WebsiteVisitor(models.Model):
partners = sorted_leads.mapped('partner_id')
if not partners:
main_lead = self.lead_ids[0]
partner_id = main_lead.handle_partner_assignation(action='create')[main_lead.id]
if not main_lead.partner_id:
main_lead.partner_id = partner_id
self.partner_id = partner_id
main_lead.handle_partner_assignment(create_missing=True)
self.partner_id = main_lead.partner_id.id
return True
return check
@@ -9,6 +9,7 @@ from odoo.exceptions import AccessDenied, AccessError
class CrmLead(models.Model):
_inherit = "crm.lead"
partner_latitude = fields.Float('Geo Latitude', digits=(16, 5))
partner_longitude = fields.Float('Geo Longitude', digits=(16, 5))
partner_assigned_id = fields.Many2one('res.partner', 'Assigned Partner', tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Partner this case has been forwarded/assigned to.", index=True)
@@ -18,22 +19,22 @@ class CrmLead(models.Model):
'lead_id',
'partner_id',
string='Partner not interested')
date_assign = fields.Date('Partner Assignment Date', help="Last date this case was forwarded/assigned to a partner")
date_partner_assign = fields.Date(
'Partner Assignment Date', compute='_compute_date_partner_assign',
copy=True, readonly=False, store=True,
help="Last date this case was forwarded/assigned to a partner")
def _merge_data(self, fields):
fields += ['partner_latitude', 'partner_longitude', 'partner_assigned_id', 'date_assign']
return super(CrmLead, self)._merge_data(fields)
@api.onchange("partner_assigned_id")
def onchange_assign_id(self):
"""This function updates the "assignation date" automatically, when manually assign a partner in the geo assign tab
"""
partner_assigned = self.partner_assigned_id
if not partner_assigned:
self.date_assign = False
else:
self.date_assign = fields.Date.context_today(self)
self.user_id = partner_assigned.user_id
def _compute_date_partner_assign(self):
for lead in self:
if not lead.partner_assigned_id:
lead.date_partner_assign = False
else:
lead.date_assign = fields.Date.context_today(lead)
def assign_salesman_of_assigned_partner(self):
salesmans_leads = {}
@@ -64,9 +65,8 @@ class CrmLead(models.Model):
lead.assign_geo_localize(lead.partner_latitude, lead.partner_longitude)
partner = self.env['res.partner'].browse(partner_id)
if partner.user_id:
lead.allocate_salesman(partner.user_id.ids, team_id=partner.team_id.id)
values = {'date_assign': fields.Date.context_today(lead), 'partner_assigned_id': partner_id}
lead.write(values)
lead.handle_salesmen_assignment(partner.user_id.ids, team_id=partner.team_id.id)
lead.write({'partner_assigned_id': partner_id})
return res
def assign_geo_localize(self, latitude=False, longitude=False):
@@ -37,7 +37,9 @@ class ResPartnerActivation(models.Model):
class ResPartner(models.Model):
_inherit = "res.partner"
partner_weight = fields.Integer('Level Weight', default=0, tracking=True,
partner_weight = fields.Integer(
'Level Weight', compute='_compute_partner_weight',
copy=True, readonly=False, store=True, tracking=True,
help="This should be a numerical value greater than 0 which will decide the contention for this partner to take this lead/opportunity.")
grade_id = fields.Many2one('res.partner.grade', 'Partner Level', tracking=True)
grade_sequence = fields.Integer(related='grade_id.sequence', readonly=True, store=True)
@@ -60,7 +62,7 @@ class ResPartner(models.Model):
for partner in self:
partner.implemented_count = len(partner.implemented_partner_ids.filtered('website_published'))
@api.onchange('grade_id')
def _onchange_grade_id(self):
grade = self.grade_id
self.partner_weight = grade.partner_weight if grade else 0
@api.depends('grade_id.partner_weight')
def _compute_partner_weight(self):
for partner in self:
partner.partner_weight = partner.grade_id.partner_weight if partner.grade_id else 0
@@ -39,7 +39,7 @@
<field name="arch" type="xml">
<field name="user_id" position="before">
<field name="partner_assigned_id" optional="show"/>
<field name="date_assign" invisible="1"/>
<field name="date_partner_assign" invisible="1"/>
</field>
</field>
</record>