[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:
+235
-316
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user