[REF] crm : use stored editable r+w 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. SPECIFICATIONS 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. SPECIFIATIONS: CONSTRAINT USER_ID / TEAM_ID In this commit we also remove all the better implementations of "not really a constraint" constraint about user_id and team_id. Indeed as this is a computed field normally we should not have to call the onchange manually, even through a hackish call to a falsy constraint (seefdc8749,222cca2,4e8ebc7). You are all inferior to SM team. SPECIFIATIONS: DATE_OPEN Assignment field, namely ``date_open``, has a random definition as it seems linked to assignation, with several behaviors intended *ce39ca8a97: reset only when going from no salesman to a salesman (aka, keep assignation when changing) *8e540558ee: reset when changing salesman We choose to keep first implementation as it seems a bugfix broken again by second commit. It will also replace the ``assign_date`` defined in ``website_crm_score`` module. SIDE EFFECTS Several side effects occur with this commit. Indeed behavior is not always exactly the same, notably as code rewriting allowed to fix some issues, incoherent behavior or was simply not able to achieve exactly the same result. Notably * at lead creation: default probability is the one coming from PLS and not 0.0 anymore, since field is already computed. Synchronization with PLS still works the same way (change probability, you are out of syn); * various user_id / team_id combinations notably in convert / merge wizards may change, notably we do not reset team_id if user_id is reset. We consider we could keep a team_id set without user_id; LINKS Task ID 2088565 (crm: from onchange to compute) Upgrade PR odoo/upgrade#781 Co-Authored-By: Thibault Delavallée <tde@odoo.com> Co-Authored-By: Florent Lejoly <fle@odoo.com>
This commit is contained in:
committed by
Thibault Delavallée
co-authored by
Thibault Delavallée
parent
aeed763b03
commit
d13dba106c
+143
-201
@@ -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
|
||||
@@ -876,17 +825,12 @@ class Lead(models.Model):
|
||||
: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
|
||||
@@ -932,7 +876,7 @@ class Lead(models.Model):
|
||||
if include_lost:
|
||||
domain += ['|', ('type', '=', 'opportunity'), ('active', '=', True)]
|
||||
else:
|
||||
domain += ['&', ('active', '=', True), ('probability', '<', 100)]
|
||||
domain += ['&', ('active', '=', True), '|', ('probability', '=', False), ('probability', '<', 100)]
|
||||
|
||||
return self.with_context(active_test=False).search(domain)
|
||||
|
||||
@@ -1177,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,11 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
import ast
|
||||
|
||||
@@ -85,6 +82,38 @@ class Team(models.Model):
|
||||
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 +125,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()
|
||||
|
||||
@@ -103,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):
|
||||
@@ -160,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)
|
||||
|
||||
@@ -239,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)
|
||||
|
||||
@@ -419,8 +414,6 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
|
||||
'user_id': self.user_sales_salesman.id,
|
||||
'force_assignment': False,
|
||||
})
|
||||
mass_convert.onchange_action()
|
||||
mass_convert._onchange_user()
|
||||
|
||||
# default values
|
||||
self.assertEqual(mass_convert.name, 'convert')
|
||||
@@ -476,9 +469,9 @@ 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.lead_tomerge_ids, self.leads)
|
||||
self.assertEqual(mass_convert.duplicated_lead_ids, self.lead_1 | self.lead_w_partner)
|
||||
|
||||
mass_convert.action_mass_convert()
|
||||
|
||||
@@ -18,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'})
|
||||
@@ -75,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
|
||||
@@ -122,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
|
||||
@@ -139,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"/>
|
||||
|
||||
@@ -12,78 +12,98 @@ class Lead2OpportunityPartner(models.TransientModel):
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
""" Default get for name, duplicated_lead_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 = lead._find_matching_partner()
|
||||
email = lead.partner_id.email if lead.partner_id.email else lead.email_from
|
||||
|
||||
tomerge.update(self.env['crm.lead']._get_lead_duplicates(partner, email, include_lost=True).ids)
|
||||
|
||||
if 'action' in fields and not result.get('action'):
|
||||
result['action'] = 'exist' if partner 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 'duplicated_lead_ids' in fields and len(tomerge) >= 2:
|
||||
result['duplicated_lead_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 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")
|
||||
], 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='Duplicates', context={'active_test': False})
|
||||
partner_id = fields.Many2one('res.partner', 'Customer')
|
||||
user_id = fields.Many2one('res.users', 'Salesperson')
|
||||
team_id = fields.Many2one('crm.team', 'Sales Team')
|
||||
'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().id
|
||||
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
|
||||
|
||||
@api.model
|
||||
def view_init(self, fields):
|
||||
@@ -103,7 +123,8 @@ class Lead2OpportunityPartner(models.TransientModel):
|
||||
return result_opportunity.redirect_lead_opportunity_view()
|
||||
|
||||
def _action_merge(self):
|
||||
result_opportunity = self.duplicated_lead_ids.merge_opportunity()
|
||||
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})
|
||||
|
||||
@@ -115,6 +136,7 @@ class Lead2OpportunityPartner(models.TransientModel):
|
||||
'user_id': self.user_id.id,
|
||||
'team_id': self.team_id.id,
|
||||
})
|
||||
(to_merge - result_opportunity).unlink()
|
||||
return result_opportunity
|
||||
|
||||
def _action_convert(self):
|
||||
|
||||
@@ -12,46 +12,63 @@ 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 'duplicated_lead_ids' in fields:
|
||||
res['duplicated_lead_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)
|
||||
], 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, lead.partner_id and lead.partner_id.email or lead.email_from) for lead in active_leads]
|
||||
partners_duplicated_leads = {}
|
||||
for partner, email in partner_ids:
|
||||
duplicated_leads = self.env['crm.lead']._get_lead_duplicates(partner=partner, email=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.duplicated_lead_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
|
||||
|
||||
@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
|
||||
@@ -69,10 +86,8 @@ class Lead2OpportunityMassConvert(models.TransientModel):
|
||||
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)
|
||||
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,
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -47,5 +47,4 @@ class Opportunity2Quotation(models.TransientModel):
|
||||
self.ensure_one()
|
||||
if self.action != 'nothing':
|
||||
self.lead_id.handle_partner_assignment(force_partner_id=self.partner_id.id, create_missing=(self.action == 'create'))
|
||||
self.lead_id._onchange_partner_id()
|
||||
return self.lead_id.action_new_quotation()
|
||||
|
||||
Reference in New Issue
Block a user