From d13dba106c2f2a3c5e6a3460820a178c9e12d1eb Mon Sep 17 00:00:00 2001 From: Florent Lejoly Date: Wed, 13 Nov 2019 14:15:25 +0000 Subject: [PATCH] [REF] crm : use stored editable r+w fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 * ce39ca8a978bfc2d4c431693fa4c56610fd14368 : reset only when going from no salesman to a salesman (aka, keep assignation when changing) * 8e540558ee1670dc32f00c6fc8cbb04838b4b972: 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 Co-Authored-By: Florent Lejoly --- addons/crm/models/crm_lead.py | 344 ++++++++---------- addons/crm/models/crm_team.py | 44 ++- addons/crm/models/res_config_settings.py | 23 +- addons/crm/models/res_users.py | 1 - addons/crm/tests/common.py | 7 +- addons/crm/tests/test_crm_lead_convert.py | 27 +- addons/crm/tests/test_crm_lead_merge.py | 10 +- addons/crm/views/crm_lead_views.xml | 2 +- addons/crm/wizard/crm_lead_to_opportunity.py | 132 ++++--- .../wizard/crm_lead_to_opportunity_mass.py | 81 +++-- .../crm_lead_to_opportunity_mass_views.xml | 1 + addons/crm/wizard/crm_merge_opportunities.py | 23 +- addons/crm_livechat/models/mail_channel.py | 4 +- .../wizard/crm_opportunity_to_quotation.py | 1 - 14 files changed, 351 insertions(+), 349 deletions(-) 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()