[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 (see fdc8749, 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:
Florent Lejoly
2020-03-23 11:42:12 +00:00
committed by Thibault Delavallée
co-authored by Thibault Delavallée
parent aeed763b03
commit d13dba106c
14 changed files with 351 additions and 349 deletions
+143 -201
View File
@@ -45,8 +45,7 @@ CRM_LEAD_FIELDS_TO_MERGE = [
'date_action_last',
'email_from',
'email_cc',
'website',
'partner_name']
'website']
class Lead(models.Model):
@@ -61,16 +60,8 @@ class Lead(models.Model):
'phone.validation.mixin']
_primary_email = 'email_from'
def _default_team_id(self, user_id):
domain = [('use_leads', '=', True)] if self._context.get('default_type') == "lead" or self.type == 'lead' else [('use_opportunities', '=', True)]
return self.env['crm.team']._get_default_team_id(user_id=user_id, domain=domain)
def _default_stage_id(self):
team = self._default_team_id(user_id=self.env.uid)
return self._stage_find(team_id=team.id, domain=[('fold', '=', False)]).id
# Description
name = fields.Char('Opportunity', required=True, index=True)
name = fields.Char('Opportunity', index=True, required=True)
user_id = fields.Many2one('res.users', string='Salesperson', index=True, tracking=True, default=lambda self: self.env.user)
user_email = fields.Char('User Email', related='user_id.email', readonly=True)
user_login = fields.Char('User Login', related='user_id.login', readonly=True)
@@ -81,22 +72,23 @@ class Lead(models.Model):
type = fields.Selection([
('lead', 'Lead'), ('opportunity', 'Opportunity')],
index=True, required=True, tracking=15,
default=lambda self: 'lead' if self.env['res.users'].has_group('crm.group_use_lead') else 'opportunity',
help="Type is used to separate Leads and Opportunities")
default=lambda self: 'lead' if self.env['res.users'].has_group('crm.group_use_lead') else 'opportunity')
priority = fields.Selection(
crm_stage.AVAILABLE_PRIORITIES, string='Priority', index=True,
default=crm_stage.AVAILABLE_PRIORITIES[0][0])
team_id = fields.Many2one(
'crm.team', string='Sales Team', index=True, tracking=True,
default=lambda self: self._default_team_id(self.env.uid))
compute='_compute_team_id', copy=True, readonly=False, store=True)
stage_id = fields.Many2one(
'crm.stage', string='Stage', ondelete='restrict',
copy=False, group_expand='_read_group_stage_ids', index=True, tracking=True,
domain="['|', ('team_id', '=', False), ('team_id', '=', team_id)]",
default=lambda self: self._default_stage_id())
'crm.stage', string='Stage', index=True, tracking=True,
compute='_compute_stage_id', readonly=False, store=True,
copy=False, group_expand='_read_group_stage_ids', ondelete='restrict',
domain="['|', ('team_id', '=', False), ('team_id', '=', team_id)]")
kanban_state = fields.Selection([
('grey', 'No next activity planned'), ('red', 'Next activity late'), ('green', 'Next activity is planned')],
string='Kanban State', compute='_compute_kanban_state')
('grey', 'No next activity planned'),
('red', 'Next activity late'),
('green', 'Next activity is planned')], string='Kanban State',
compute='_compute_kanban_state')
tag_ids = fields.Many2many(
'crm.tag', 'crm_tag_rel', 'lead_id', 'tag_id', string='Tags',
help="Classify and analyze your lead/opportunity categories like: Training, Service")
@@ -108,26 +100,37 @@ class Lead(models.Model):
# Dates
date_closed = fields.Datetime('Closed Date', readonly=True, copy=False)
date_action_last = fields.Datetime('Last Action', readonly=True)
date_open = fields.Datetime('Assignment Date', readonly=True, default=fields.Datetime.now)
date_open = fields.Datetime(
'Assignment Date', compute='_compute_date_open', readonly=True, store=True)
day_open = fields.Float('Days to Assign', compute='_compute_day_open', store=True)
day_close = fields.Float('Days to Close', compute='_compute_day_close', store=True)
date_last_stage_update = fields.Datetime('Last Stage Update', index=True, default=fields.Datetime.now)
date_last_stage_update = fields.Datetime(
'Last Stage Update', compute='_compute_date_last_stage_update', index=True, readonly=True, store=True)
date_conversion = fields.Datetime('Conversion Date', readonly=True)
date_deadline = fields.Date('Expected Closing', help="Estimate of the date on which the opportunity will be won.")
# Customer / contact
partner_id = fields.Many2one(
'res.partner', string='Customer', tracking=10, index=True,
'res.partner', string='Customer', index=True, tracking=10,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
help="Linked partner (optional). Usually created when converting the lead. You can find a partner by its Name, TIN, Email or Internal Reference.")
partner_address_email = fields.Char('Partner Contact Email', related='partner_id.email', readonly=True)
partner_is_blacklisted = fields.Boolean('Partner is blacklisted', related='partner_id.is_blacklisted', readonly=True)
contact_name = fields.Char('Contact Name', tracking=30)
partner_name = fields.Char("Company Name", tracking=20, index=True, help='The name of the future partner company that will be created while converting the lead into opportunity')
function = fields.Char('Job Position')
title = fields.Many2one('res.partner.title', string='Title')
email_from = fields.Char('Email', index=True, tracking=40)
phone = fields.Char('Phone', tracking=50)
mobile = fields.Char('Mobile')
contact_name = fields.Char(
'Contact Name', tracking=30,
compute='_compute_partner_id_values', readonly=False, store=True)
partner_name = fields.Char(
'Company Name', tracking=20, index=True,
compute='_compute_partner_id_values', readonly=False, store=True,
help='The name of the future partner company that will be created while converting the lead into opportunity')
function = fields.Char('Job Position', compute='_compute_partner_id_values', readonly=False, store=True)
title = fields.Many2one('res.partner.title', string='Title',compute='_compute_partner_id_values', readonly=False, store=True)
email_from = fields.Char(
'Email', tracking=40, index=True,
compute='_compute_partner_id_values', readonly=False, store=True)
phone = fields.Char(
'Phone', tracking=50,
compute='_compute_partner_id_values', readonly=False, store=True)
mobile = fields.Char('Mobile', compute='_compute_partner_id_values', readonly=False, store=True)
phone_mobile_search = fields.Char('Phone/Mobile', store=False, search='_search_phone_mobile_search')
phone_state = fields.Selection([
('correct', 'Correct'),
@@ -135,18 +138,25 @@ class Lead(models.Model):
email_state = fields.Selection([
('correct', 'Correct'),
('incorrect', 'Incorrect')], string='Email Quality', compute="_compute_email_state", store=True)
website = fields.Char('Website', index=True, help="Website of the contact")
website = fields.Char('Website', index=True, help="Website of the contact", compute="_compute_partner_id_values", store=True, readonly=False)
lang_id = fields.Many2one('res.lang', string='Language')
# Address fields
street = fields.Char('Street')
street2 = fields.Char('Street2')
zip = fields.Char('Zip', change_default=True)
city = fields.Char('City')
state_id = fields.Many2one("res.country.state", string='State', domain="[('country_id', '=?', country_id)]")
country_id = fields.Many2one('res.country', string='Country')
street = fields.Char('Street', compute='_compute_partner_id_values', readonly=False, store=True)
street2 = fields.Char('Street2', compute='_compute_partner_id_values', readonly=False, store=True)
zip = fields.Char('Zip', change_default=True, compute='_compute_partner_id_values', readonly=False, store=True)
city = fields.Char('City', compute='_compute_partner_id_values', readonly=False, store=True)
state_id = fields.Many2one(
"res.country.state", string='State',
compute='_compute_partner_id_values', readonly=False, store=True,
domain="[('country_id', '=?', country_id)]")
country_id = fields.Many2one(
'res.country', string='Country',
compute='_compute_partner_id_values', readonly=False, store=True)
# Probability (Opportunity only)
probability = fields.Float('Probability', group_operator="avg", copy=False)
automated_probability = fields.Float('Automated Probability', readonly=True)
probability = fields.Float(
'Probability', group_operator="avg", copy=False,
compute='_compute_probabilities', readonly=False, store=True)
automated_probability = fields.Float('Automated Probability', compute='_compute_probabilities', readonly=True, store=True)
is_automated_probability = fields.Boolean('Is automated probability?', compute="_compute_is_automated_probability")
# External records
meeting_count = fields.Integer('# Meetings', compute='_compute_meeting_count')
@@ -170,12 +180,38 @@ class Lead(models.Model):
kanban_state = 'red'
lead.kanban_state = kanban_state
@api.depends('planned_revenue', 'probability')
def _compute_expected_revenue(self):
@api.depends('user_id', 'type')
def _compute_team_id(self):
""" When changing the user, also set a team_id or restrict team id
to the ones user_id is member of. """
for lead in self:
lead.expected_revenue = round((lead.planned_revenue or 0.0) * (lead.probability or 0) / 100.0, 2)
# setting user as void should not trigger a new team computation
if not lead.user_id:
continue
user = lead.user_id
if lead.team_id and user in lead.team_id.member_ids | lead.team_id.user_id:
continue
team_domain = [('use_leads', '=', True)] if lead.type == 'lead' else [('use_opportunities', '=', True)]
team = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=team_domain)
lead.team_id = team.id
@api.depends('date_open')
@api.depends('team_id', 'type')
def _compute_stage_id(self):
for lead in self:
if not lead.stage_id:
lead.stage_id = lead._stage_find(domain=[('fold', '=', False)]).id
@api.depends('user_id')
def _compute_date_open(self):
for lead in self:
lead.date_open = fields.Datetime.now() if lead.user_id else False
@api.depends('stage_id')
def _compute_date_last_stage_update(self):
for lead in self:
lead.date_last_stage_update = fields.Datetime.now()
@api.depends('create_date', 'date_open')
def _compute_day_open(self):
""" Compute difference between create date and open date """
leads = self.filtered(lambda l: l.date_open and l.create_date)
@@ -186,7 +222,7 @@ class Lead(models.Model):
date_open = fields.Datetime.from_string(lead.date_open)
lead.day_open = abs((date_open - date_create).days)
@api.depends('date_closed')
@api.depends('create_date', 'date_closed')
def _compute_day_close(self):
""" Compute difference between current date and log date """
leads = self.filtered(lambda l: l.date_closed and l.create_date)
@@ -197,13 +233,11 @@ class Lead(models.Model):
date_close = fields.Datetime.from_string(lead.date_closed)
lead.day_close = abs((date_close - date_create).days)
@api.depends('probability', 'automated_probability')
def _compute_is_automated_probability(self):
""" If probability and automated_probability are equal, the probability is considered as automatic.
when the probability is in automatic mode, the field probability will be aligned
when automated_probability is modified. """
@api.depends('partner_id')
def _compute_partner_id_values(self):
""" compute the new values when partner_id has changed """
for lead in self:
lead.is_automated_probability = tools.float_compare(lead.probability, lead.automated_probability, 2) == 0
lead.update(lead._preare_values_from_partner(lead.partner_id))
@api.depends('phone', 'country_id.code')
def _compute_phone_state(self):
@@ -230,6 +264,33 @@ class Lead(models.Model):
break
lead.email_state = email_state
@api.depends('probability', 'automated_probability')
def _compute_is_automated_probability(self):
""" If probability and automated_probability are equal probability computation
is considered as automatic, aka probability is sync with automated_probability """
for lead in self:
# creation mode: consider it as being not automated
if not lead.id and not lead._origin.id:
lead.is_automated_probability = False
else:
lead.is_automated_probability = tools.float_compare(lead.probability, lead.automated_probability, 2) == 0
@api.depends(lambda self: ['tag_ids', 'stage_id', 'team_id'] + self._pls_get_safe_fields())
def _compute_probabilities(self):
for lead in self:
was_automated = False
lead_probabilities = lead._pls_get_naive_bayes_probabilities()
if lead.id in lead_probabilities:
was_automated = lead.active and lead.is_automated_probability
lead.automated_probability = lead_probabilities[lead.id]
if was_automated:
lead.probability = lead.automated_probability
@api.depends('planned_revenue', 'probability')
def _compute_expected_revenue(self):
for lead in self:
lead.expected_revenue = round((lead.planned_revenue or 0.0) * (lead.probability or 0) / 100.0, 2)
def _compute_meeting_count(self):
meeting_data = self.env['calendar.event'].read_group([('opportunity_id', 'in', self.ids)], ['opportunity_id'], ['opportunity_id'])
mapped_data = {m['opportunity_id'][0]: m['opportunity_id_count'] for m in meeting_data}
@@ -261,121 +322,6 @@ class Lead(models.Model):
return [(0, '=', 1)]
return [('id', 'in', [r[0] for r in res])]
def _onchange_partner_id_values(self, partner_id):
""" returns the new values when partner_id has changed """
if partner_id:
partner = self.env['res.partner'].browse(partner_id)
partner_name = partner.parent_id.name
if not partner_name and partner.is_company:
partner_name = partner.name
return {
'partner_name': partner_name,
'contact_name': partner.name if not partner.is_company else False,
'title': partner.title.id,
'street': partner.street,
'street2': partner.street2,
'city': partner.city,
'state_id': partner.state_id.id,
'country_id': partner.country_id.id,
'email_from': partner.email,
'phone': partner.phone,
'mobile': partner.mobile,
'zip': partner.zip,
'function': partner.function,
'website': partner.website,
}
return {}
@api.onchange('partner_id')
def _onchange_partner_id(self):
values = self._onchange_partner_id_values(self.partner_id.id if self.partner_id else False)
self.update(values)
@api.model
def _onchange_user_values(self, user_id):
""" returns new values when user_id has changed """
if not user_id:
return {}
if user_id and self._context.get('team_id'):
team = self.env['crm.team'].browse(self._context['team_id'])
if user_id in team.member_ids.ids or user_id == team.user_id.id:
return {}
team_id = self._default_team_id(user_id)
return {'team_id': team_id}
@api.onchange('user_id')
def _onchange_user_id(self):
""" When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """
if self.user_id.sale_team_id:
values = self._onchange_user_values(self.user_id.id)
self.update(values)
def _onchange_compute_probability(self, optional_field_name=None):
"""Recompute probability on onchange methods of :
'stage_id', 'team_id', 'tag_ids'
'country_id', 'state_id', 'phone_state', 'email_state', 'source_id' """
if optional_field_name and optional_field_name not in self._pls_get_safe_fields():
return
lead_probabilities = self._pls_get_naive_bayes_probabilities()
if self.id in lead_probabilities:
self.automated_probability = lead_probabilities[self.id]
if self._origin.is_automated_probability:
self.probability = self.automated_probability
@api.onchange('stage_id')
def _onchange_stage_id(self):
self._onchange_compute_probability()
@api.onchange('source_id')
def _onchange_source_id(self):
self._onchange_compute_probability(optional_field_name='source_id')
@api.onchange('automated_probability')
def _onchange_automated_probability(self):
""" If was in auto mode before the change, align both fields so that it will stay in auto mode. """
if self._origin.is_automated_probability:
self.probability = self.automated_probability
@api.onchange('team_id')
def _onchange_team_id(self):
self._onchange_compute_probability()
@api.onchange('tag_ids')
def _onchange_tag_ids(self):
self._onchange_compute_probability()
@api.onchange('phone_state')
def _onchange_phone_state(self):
self._onchange_compute_probability(optional_field_name='phone_state')
@api.onchange('email_state')
def _onchange_email_state(self):
self._onchange_compute_probability(optional_field_name='email_state')
@api.constrains('user_id')
def _valid_team(self):
for lead in self:
if lead.user_id:
values = lead.with_context(team_id=lead.team_id.id)._onchange_user_values(lead.user_id.id)
if values:
lead.update(values)
@api.onchange('state_id')
def _onchange_state(self):
self._onchange_compute_probability(optional_field_name='state_id')
if self.state_id:
self.country_id = self.state_id.country_id.id
@api.onchange('country_id')
def _onchange_country_id(self):
self._onchange_compute_probability(optional_field_name='country_id')
@api.onchange('lang_id')
def _onchange_lang_id(self):
self._onchange_compute_probability(optional_field_name='lang_id')
@api.onchange('phone', 'country_id', 'company_id')
def _onchange_phone_validation(self):
if self.phone:
@@ -386,6 +332,30 @@ class Lead(models.Model):
if self.mobile:
self.mobile = self.phone_format(self.mobile)
def _preare_values_from_partner(self, partner):
""" Get a dictionary with values coming from customer information to
copy on a lead. Email_from and phone fields get the current lead
values to avoid being reset if customer has no value for them. """
partner_name = partner.parent_id.name
if not partner_name and partner.is_company:
partner_name = partner.name
return {
'partner_name': partner_name,
'contact_name': partner.name if not partner.is_company else False,
'title': partner.title.id,
'street': partner.street,
'street2': partner.street2,
'city': partner.city,
'state_id': partner.state_id.id,
'country_id': partner.country_id.id,
'email_from': partner.email or self.email_from,
'phone': partner.phone or self.phone,
'mobile': partner.mobile,
'zip': partner.zip,
'function': partner.function,
'website': partner.website,
}
# ------------------------------------------------------------
# ORM
# ------------------------------------------------------------
@@ -400,44 +370,23 @@ class Lead(models.Model):
@api.model
def create(self, vals):
# set up context used to find the lead's Sales Team which is needed
# to correctly set the default stage_id
context = dict(self._context or {})
if vals.get('type') and not self._context.get('default_type'):
context['default_type'] = vals.get('type')
if vals.get('team_id') and not self._context.get('default_team_id'):
context['default_team_id'] = vals.get('team_id')
if vals.get('user_id') and 'date_open' not in vals:
vals['date_open'] = fields.Datetime.now()
partner_id = vals.get('partner_id') or context.get('default_partner_id')
onchange_values = self._onchange_partner_id_values(partner_id)
onchange_values.update(vals) # we don't want to overwrite any existing key
vals = onchange_values
result = super(Lead, self.with_context(context)).create(vals)
lead = super(Lead, self).create(vals)
# Compute new probability for each lead separately
result._update_probability()
return result
lead._update_probability()
return lead
def write(self, vals):
# stage change:
if 'stage_id' in vals:
vals['date_last_stage_update'] = fields.Datetime.now()
stage_id = self.env['crm.stage'].browse(vals['stage_id'])
if stage_id.is_won:
vals.update({'probability': 100})
# Only write the 'date_open' if no salesperson was assigned.
if vals.get('user_id') and 'date_open' not in vals and not self.mapped('user_id'):
vals['date_open'] = fields.Datetime.now()
# stage change with new stage: update probability and date_closed
if vals.get('probability', 0) >= 100 or not vals.get('active', True):
vals['date_closed'] = fields.Datetime.now()
elif 'probability' in vals:
vals['date_closed'] = False
if vals.get('user_id') and 'date_open' not in vals:
vals['date_open'] = fields.Datetime.now()
write_result = super(Lead, self).write(vals)
# Compute new automated_probability (and, eventually, probability) for each lead separately
@@ -452,7 +401,7 @@ class Lead(models.Model):
if lead.id in lead_probabilities:
lead_proba = lead_probabilities[lead.id]
proba_vals = {'automated_probability': lead_proba}
if tools.float_compare(lead.probability, lead.automated_probability, 2) == 0:
if lead.active and lead.is_automated_probability:
proba_vals['probability'] = lead_proba
super(Lead, lead).write(proba_vals)
return
@@ -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)
+35 -9
View File
@@ -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
+14 -9
View File
@@ -2,14 +2,17 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from datetime import date
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
crm_alias_prefix = fields.Char('Default Alias Name for Leads')
generate_lead_from_alias = fields.Boolean('Manual Assignment of Emails', config_parameter='crm.generate_lead_from_alias')
crm_alias_prefix = fields.Char(
'Default Alias Name for Leads',
compute="_compute_crm_alias_prefix" , readonly=False, store=True)
generate_lead_from_alias = fields.Boolean(
'Manual Assignment of Emails', config_parameter='crm.generate_lead_from_alias',
compute="_compute_generate_lead_from_alias", readonly=False, store=True)
group_use_lead = fields.Boolean(string="Leads", implied_group='crm.group_use_lead')
module_crm_iap_lead = fields.Boolean("Generate new leads based on their country, industries, size, etc.")
module_crm_iap_lead_website = fields.Boolean("Create Leads/Opportunities from your website's traffic")
@@ -71,15 +74,17 @@ class ResConfigSettings(models.TransientModel):
if setting.predictive_lead_scoring_start_date:
setting.predictive_lead_scoring_start_date_str = fields.Date.to_string(setting.predictive_lead_scoring_start_date)
@api.onchange('group_use_lead')
def _onchange_group_use_lead(self):
@api.depends('group_use_lead')
def _compute_generate_lead_from_alias(self):
""" Reset alias / leads configuration if leads are not used """
if not self.group_use_lead:
self.generate_lead_from_alias = False
for setting in self:
setting.generate_lead_from_alias = False
@api.onchange('generate_lead_from_alias')
def _onchange_generate_lead_from_alias(self):
self.crm_alias_prefix = (self.crm_alias_prefix or 'info') if self.generate_lead_from_alias else False
@api.depends('generate_lead_from_alias')
def _compute_crm_alias_prefix(self):
for setting in self:
setting.crm_alias_prefix = (setting.crm_alias_prefix or 'info') if setting.generate_lead_from_alias else False
@api.model
def get_values(self):
-1
View File
@@ -5,7 +5,6 @@ from odoo import fields, models
class Users(models.Model):
_inherit = 'res.users'
target_sales_won = fields.Integer('Won in Opportunities Target')
+5 -2
View File
@@ -116,6 +116,10 @@ class TestCrmCommon(SavepointCase, MailCase):
'contact_name': 'Amy Wong',
'email_from': 'amy.wong@test.example.com',
})
# update lead_1: stage_id is not computed anymore by default for leads
cls.lead_1.write({
'stage_id': cls.stage_team1_1.id,
})
# create an history for new team
cls.lead_team_1_won = cls.env['crm.lead'].create({
@@ -303,6 +307,7 @@ class TestLeadConvertMassCommon(TestLeadConvertCommon):
'stage_id': False,
'partner_id': cls.contact_1.id,
})
cls.lead_w_partner.write({'stage_id': False})
cls.lead_w_partner_company = cls.env['crm.lead'].create({
'name': 'New1',
'type': 'lead',
@@ -338,6 +343,4 @@ class TestLeadConvertMassCommon(TestLeadConvertCommon):
'stage_id': cls.stage_team1_2.id,
'active': False,
})
for l in (cls.lead_w_partner | cls.lead_w_partner_company | cls.lead_w_contact | cls.lead_w_email | cls.lead_w_email_lost):
l._onchange_user_id()
(cls.lead_w_partner | cls.lead_w_partner_company | cls.lead_w_contact | cls.lead_w_email | cls.lead_w_email_lost).flush()
+10 -17
View File
@@ -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()
+4 -6
View File
@@ -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)
+1 -1
View File
@@ -172,7 +172,7 @@
</group>
<group>
<field name="user_id" domain="[('share', '=', False)]"
context="{'team_id': team_id}"/>
context="{'default_sales_team_id': team_id}"/>
<field name="team_id" widget="selection"
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]"/>
<field name="type" invisible="1"/>
+77 -55
View File
@@ -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"/>
+12 -11
View File
@@ -34,22 +34,23 @@ class MergeOpportunity(models.TransientModel):
opportunity_ids = fields.Many2many('crm.lead', 'merge_opportunity_rel', 'merge_id', 'opportunity_id', string='Leads/Opportunities')
user_id = fields.Many2one('res.users', 'Salesperson', index=True)
team_id = fields.Many2one('crm.team', 'Sales Team', index=True)
team_id = fields.Many2one(
'crm.team', 'Sales Team', index=True,
compute='_compute_team_id', readonly=False, store=True)
def action_merge(self):
self.ensure_one()
merge_opportunity = self.opportunity_ids.merge_opportunity(self.user_id.id, self.team_id.id)
return merge_opportunity.redirect_lead_opportunity_view()
@api.onchange('user_id')
def _onchange_user(self):
@api.depends('user_id')
def _compute_team_id(self):
""" When changing the user, also set a team_id or restrict team id
to the ones user_id is member of. """
team_id = False
if self.user_id:
user_in_team = False
if self.team_id:
user_in_team = self.env['crm.team'].search_count([('id', '=', self.team_id.id), '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id)])
if not user_in_team:
team_id = self.env['crm.team'].search(['|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id)], limit=1)
self.team_id = team_id
for wizard in self:
if wizard.user_id:
user_in_team = False
if wizard.team_id:
user_in_team = wizard.env['crm.team'].search_count([('id', '=', wizard.team_id.id), '|', ('user_id', '=', wizard.user_id.id), ('member_ids', '=', wizard.user_id.id)])
if not user_in_team:
wizard.team_id = wizard.env['crm.team'].search(['|', ('user_id', '=', wizard.user_id.id), ('member_ids', '=', wizard.user_id.id)], limit=1)
+1 -3
View File
@@ -31,7 +31,7 @@ class MailChannel(models.Model):
for message in self.channel_message_ids.sorted('id')
)
utm_source = self.env.ref('crm_livechat.utm_source_livechat', raise_if_not_found=False)
lead = self.env['crm.lead'].create({
return self.env['crm.lead'].create({
'name': html2plaintext(key[5:]),
'partner_id': channel_partners.partner_id.id,
'user_id': None,
@@ -40,5 +40,3 @@ class MailChannel(models.Model):
'referred': partner.name,
'source_id': utm_source and utm_source.id,
})
lead._onchange_partner_id()
return lead
@@ -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()