diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 53abc9a4130..c591a62a77e 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -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) diff --git a/addons/crm/models/crm_team.py b/addons/crm/models/crm_team.py index f6a2b1c83b3..cb144064e16 100644 --- a/addons/crm/models/crm_team.py +++ b/addons/crm/models/crm_team.py @@ -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 diff --git a/addons/crm/models/res_config_settings.py b/addons/crm/models/res_config_settings.py index 3861c1c411f..258caf44d56 100644 --- a/addons/crm/models/res_config_settings.py +++ b/addons/crm/models/res_config_settings.py @@ -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): diff --git a/addons/crm/models/res_users.py b/addons/crm/models/res_users.py index 2c94f1c05ab..951287d5958 100644 --- a/addons/crm/models/res_users.py +++ b/addons/crm/models/res_users.py @@ -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') diff --git a/addons/crm/tests/common.py b/addons/crm/tests/common.py index 2fbfcc0affa..c1171e4c44d 100644 --- a/addons/crm/tests/common.py +++ b/addons/crm/tests/common.py @@ -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() diff --git a/addons/crm/tests/test_crm_lead_convert.py b/addons/crm/tests/test_crm_lead_convert.py index 3ebea3611af..75801e4a39f 100644 --- a/addons/crm/tests/test_crm_lead_convert.py +++ b/addons/crm/tests/test_crm_lead_convert.py @@ -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() diff --git a/addons/crm/tests/test_crm_lead_merge.py b/addons/crm/tests/test_crm_lead_merge.py index d959b9a43b3..1bcf1c2a640 100644 --- a/addons/crm/tests/test_crm_lead_merge.py +++ b/addons/crm/tests/test_crm_lead_merge.py @@ -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) diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 4b177ca273a..89459f4d434 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -172,7 +172,7 @@ + context="{'default_sales_team_id': team_id}"/> diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index c47566a60ef..2d7b8b77489 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -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): diff --git a/addons/crm/wizard/crm_lead_to_opportunity_mass.py b/addons/crm/wizard/crm_lead_to_opportunity_mass.py index ce1e2a0b766..f33abf83ea9 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity_mass.py +++ b/addons/crm/wizard/crm_lead_to_opportunity_mass.py @@ -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, diff --git a/addons/crm/wizard/crm_lead_to_opportunity_mass_views.xml b/addons/crm/wizard/crm_lead_to_opportunity_mass_views.xml index 0d0911df92b..d83c27b045a 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity_mass_views.xml +++ b/addons/crm/wizard/crm_lead_to_opportunity_mass_views.xml @@ -5,6 +5,7 @@ crm.lead2opportunity.partner.mass
+ diff --git a/addons/crm/wizard/crm_merge_opportunities.py b/addons/crm/wizard/crm_merge_opportunities.py index 3693df8c0e4..7307bd37554 100644 --- a/addons/crm/wizard/crm_merge_opportunities.py +++ b/addons/crm/wizard/crm_merge_opportunities.py @@ -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) diff --git a/addons/crm_livechat/models/mail_channel.py b/addons/crm_livechat/models/mail_channel.py index 6df38d2317c..f608bd1a0dd 100644 --- a/addons/crm_livechat/models/mail_channel.py +++ b/addons/crm_livechat/models/mail_channel.py @@ -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 diff --git a/addons/sale_crm/wizard/crm_opportunity_to_quotation.py b/addons/sale_crm/wizard/crm_opportunity_to_quotation.py index aa465066f8d..aef9a259ff8 100644 --- a/addons/sale_crm/wizard/crm_opportunity_to_quotation.py +++ b/addons/sale_crm/wizard/crm_opportunity_to_quotation.py @@ -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()