From 8e540558ee1670dc32f00c6fc8cbb04838b4b972 Mon Sep 17 00:00:00 2001 From: David Beguin Date: Wed, 22 May 2019 08:56:36 +0000 Subject: [PATCH] [IMP-REM] crm, * : remove stage based probability and add won_stage bool field on crm.stage This commit prepares the next Predictive Lead Scoring (aka PLS) ones. With the new PLS implementation, stage based probablility is not relevant anymore as the probability will be computed based on multiple criteria following the Naive Bayes probabilist theorem. To determine if the stage is a won stage, a new 'won_stage' boolean field replaces the old probability field that is now removed. won_stage = True is equivalent to stage.probability = 100. Task ID : 1925439 PR #33589 --- addons/crm/data/crm_stage_data.xml | 9 +-- addons/crm/models/crm_lead.py | 68 ++++--------------- addons/crm/models/crm_stage.py | 5 +- .../crm/report/crm_activity_report_views.xml | 2 +- addons/crm/tests/test_crm_lead.py | 2 +- addons/crm/views/crm_lead_views.xml | 2 +- addons/crm/views/crm_stage_views.xml | 11 ++- .../controllers/main.py | 4 +- .../models/crm_lead.py | 2 +- 9 files changed, 26 insertions(+), 79 deletions(-) diff --git a/addons/crm/data/crm_stage_data.xml b/addons/crm/data/crm_stage_data.xml index 81c69cf5697..4f24ec2e511 100644 --- a/addons/crm/data/crm_stage_data.xml +++ b/addons/crm/data/crm_stage_data.xml @@ -2,27 +2,20 @@ New - 10 - 1 1 Qualified - 30 - 1 2 Proposition - 70 - 1 3 Won - 100 - 1 + True 70 \ No newline at end of file diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index efba9117dc8..4b869d65ca7 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -53,15 +53,6 @@ class Lead(models.Model): _inherit = ['mail.thread.cc', 'mail.thread.blacklist', 'mail.activity.mixin', 'utm.mixin', 'format.address.mixin'] _primary_email = 'email_from' - def _default_probability(self): - if 'default_stage_id' in self._context: - stage_id = self._context.get('default_stage_id') - else: - stage_id = self._default_stage_id() - if stage_id: - return self.env['crm.stage'].browse(stage_id).probability - return 10 - def _default_stage_id(self): team = self.env['crm.team'].sudo()._get_default_team_id(user_id=self.env.uid) return self._stage_find(team_id=team.id, domain=[('fold', '=', False)]).id @@ -100,7 +91,7 @@ class Lead(models.Model): date_conversion = fields.Datetime('Conversion Date', readonly=True) # Only used for type opportunity - probability = fields.Float('Probability', group_operator="avg", default=lambda self: self._default_probability()) + probability = fields.Float('Probability', group_operator="avg", default=10.0) planned_revenue = fields.Monetary('Expected Revenue', currency_field='company_currency', tracking=True) expected_revenue = fields.Monetary('Prorated Revenue', currency_field='company_currency', store=True, compute="_compute_expected_revenue") date_deadline = fields.Date('Expected Closing', help="Estimate of the date on which the opportunity will be won.") @@ -196,21 +187,6 @@ class Lead(models.Model): for lead in self: lead.meeting_count = mapped_data.get(lead.id, 0) - @api.model - def _onchange_stage_id_values(self, stage_id): - """ returns the new values when stage_id has changed """ - if not stage_id: - return {} - stage = self.env['crm.stage'].browse(stage_id) - if stage.on_change: - return {'probability': stage.probability} - return {} - - @api.onchange('stage_id') - def _onchange_stage_id(self): - values = self._onchange_stage_id_values(self.stage_id.id) - self.update(values) - def _onchange_partner_id_values(self, partner_id): """ returns the new values when partner_id has changed """ if partner_id: @@ -299,14 +275,6 @@ class Lead(models.Model): # ORM override (CRUD, fields_view_get, ...) # ---------------------------------------- - @api.model - def name_create(self, name): - res = super(Lead, self).name_create(name) - - # update the probability of the lead if the stage is set to update it automatically - self.browse(res[0])._onchange_stage_id() - return res - @api.model def create(self, vals): # set up context used to find the lead's Sales Team which is needed @@ -329,19 +297,22 @@ class Lead(models.Model): @api.multi def write(self, vals): - # stage change: update date_last_stage_update + # 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('stage_id') and 'probability' not in vals: - vals.update(self._onchange_stage_id_values(vals.get('stage_id'))) 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() return super(Lead, self).write(vals) @api.multi @@ -385,7 +356,7 @@ class Lead(models.Model): def action_set_won(self): """ Won semantic: probability = 100 (active untouched) """ for lead in self: - stage_id = lead._stage_find(domain=[('probability', '=', 100.0), ('on_change', '=', True)]) + stage_id = lead._stage_find(domain=[('is_won', '=', True)]) lead.write({'stage_id': stage_id.id, 'probability': 100}) return True @@ -478,14 +449,6 @@ class Lead(models.Model): 'context': {'default_type': 'opportunity'} } - def toggle_active(self): - """ When re-activating leads and opportunities set their probability - to the default stage one. """ - res = super(Lead, self).toggle_active() - for lead in self.filtered(lambda lead: lead.active and lead.stage_id.probability): - lead.probability = lead.stage_id.probability - return res - # ---------------------------------------- # Business Methods # ---------------------------------------- @@ -686,13 +649,10 @@ class Lead(models.Model): raise UserError(_('Please select more than one element (lead or opportunity) from the list view.')) # Sorting the leads/opps according to the confidence level of its stage, which relates to the probability of winning it - # The confidence level increases with the stage sequence, except when the stage probability is 0.0 (Lost cases) - # An Opportunity always has higher confidence level than a lead, unless its stage probability is 0.0 + # The confidence level increases with the stage sequence + # An Opportunity always has higher confidence level than a lead def opps_key(opportunity): - sequence = -1 - if opportunity.stage_id.on_change: - sequence = opportunity.stage_id.sequence - return (sequence != -1 and opportunity.type == 'opportunity'), sequence, -opportunity.id + return opportunity.type == 'opportunity', opportunity.stage_id.sequence, -opportunity.id opportunities = self.sorted(key=opps_key, reverse=True) # get SORTED recordset of head and tail, and complete list @@ -778,8 +738,6 @@ class Lead(models.Model): if not self.stage_id: stage = self._stage_find(team_id=team_id) value['stage_id'] = stage.id - if stage: - value['probability'] = stage.probability return value @api.multi @@ -1042,7 +1000,7 @@ class Lead(models.Model): if date_deadline < today: result['activity']['overdue'] += 1 # Won in Opportunities - if opp.date_closed and opp.stage_id.probability == 100: + if opp.date_closed and opp.stage_id.is_won: date_closed = fields.Date.from_string(opp.date_closed) if today.replace(day=1) <= date_closed <= today: if opp.planned_revenue: @@ -1122,7 +1080,7 @@ class Lead(models.Model): @api.multi def _track_subtype(self, init_values): self.ensure_one() - if 'stage_id' in init_values and self.probability == 100 and self.stage_id and self.stage_id.on_change: + if 'stage_id' in init_values and self.probability == 100 and self.stage_id: return self.env.ref('crm.mt_lead_won') elif 'active' in init_values and self.probability == 0 and not self.active: return self.env.ref('crm.mt_lead_lost') diff --git a/addons/crm/models/crm_stage.py b/addons/crm/models/crm_stage.py index d324a3fc00a..6902a9b4ccd 100644 --- a/addons/crm/models/crm_stage.py +++ b/addons/crm/models/crm_stage.py @@ -34,8 +34,7 @@ class Stage(models.Model): name = fields.Char('Stage Name', required=True, translate=True) sequence = fields.Integer('Sequence', default=1, help="Used to order stages. Lower is better.") - probability = fields.Float('Probability (%)', required=True, default=10.0, help="This percentage depicts the default/average probability of the Case for this stage to be a success") - on_change = fields.Boolean('Change Probability Automatically', help="Setting this stage will change the probability automatically on the opportunity.") + is_won = fields.Boolean('Is Won Stage?') requirements = fields.Text('Requirements', help="Enter here the internal requirements for this stage (ex: Offer sent to customer). It will appear as a tooltip over the stage's name.") team_id = fields.Many2one('crm.team', string='Sales Team', ondelete='set null', help='Specific team that uses this stage. Other teams will not be able to see or use this stage.') @@ -44,7 +43,7 @@ class Stage(models.Model): fold = fields.Boolean('Folded in Pipeline', help='This stage is folded in the kanban view when there are no records in that stage to display.') - #This field for interface only + # This field for interface only team_count = fields.Integer('team_count', compute='_compute_team_count') @api.multi diff --git a/addons/crm/report/crm_activity_report_views.xml b/addons/crm/report/crm_activity_report_views.xml index b143b0ad713..8e751d916ea 100644 --- a/addons/crm/report/crm_activity_report_views.xml +++ b/addons/crm/report/crm_activity_report_views.xml @@ -52,7 +52,7 @@ + domain="['&', ('stage_id.is_won', '=', True)]"/> diff --git a/addons/crm/tests/test_crm_lead.py b/addons/crm/tests/test_crm_lead.py index 8e701932050..69100f463bf 100644 --- a/addons/crm/tests/test_crm_lead.py +++ b/addons/crm/tests/test_crm_lead.py @@ -40,7 +40,7 @@ class TestCRMLead(TestCrmCases): # Now I change the stage of opportunity to won. lead.action_set_won() # I check stage of opp should won, after change stage. - stage_id = lead._stage_find(domain=[('probability', '=', 100.0)]) + stage_id = lead._stage_find(domain=[('is_won', '=', True)]) self.assertEqual(stage_id, lead.stage_id, "Stage of opportunity is incorrect!") def test_crm_lead_message(self): diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 5d6064fbccc..65372748bcf 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -753,7 +753,7 @@ - + - + @@ -21,7 +21,7 @@ - + @@ -44,14 +44,11 @@ - - - - - + + diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index 429e8f9d822..3acffdacd50 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -102,7 +102,7 @@ class WebsiteAccount(CustomerPortal): 'week': {'label': _('This Week Activities'), 'domain': [('activity_date_deadline', '>=', today), ('activity_date_deadline', '<=', this_week_end_date)]}, 'overdue': {'label': _('Overdue Activities'), 'domain': [('activity_date_deadline', '<', today)]}, - 'won': {'label': _('Won'), 'domain': [('stage_id.probability', '=', 100), ('stage_id.on_change', '=', True)]}, + 'won': {'label': _('Won'), 'domain': [('stage_id.is_won', '=', True)]}, 'lost': {'label': _('Lost'), 'domain': [('active', '=', False), ('probability', '=', 0)]}, } searchbar_sortings = { @@ -170,7 +170,7 @@ class WebsiteAccount(CustomerPortal): "website_crm_partner_assign.portal_my_opportunity", { 'opportunity': opp, 'user_activity': opp.sudo().activity_ids.filtered(lambda activity: activity.user_id == request.env.user)[:1], - 'stages': request.env['crm.stage'].search([('probability', '!=', '100')], order='sequence desc, name desc, id desc'), + 'stages': request.env['crm.stage'].search([('is_won', '!=', True)], order='sequence desc, name desc, id desc'), 'activity_types': request.env['mail.activity.type'].sudo().search([]), 'states': request.env['res.country.state'].sudo().search([]), 'countries': request.env['res.country'].sudo().search([]), diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py index 153aa703939..4bca5f4878d 100644 --- a/addons/website_crm_partner_assign/models/crm_lead.py +++ b/addons/website_crm_partner_assign/models/crm_lead.py @@ -40,7 +40,7 @@ class CrmLead(models.Model): def assign_salesman_of_assigned_partner(self): salesmans_leads = {} for lead in self: - if (lead.stage_id.probability > 0 and lead.stage_id.probability < 100) or lead.stage_id.sequence == 1: + if (lead.probability > 0 and lead.probability < 100) or lead.stage_id.sequence == 1: if lead.partner_assigned_id and lead.partner_assigned_id.user_id != lead.user_id: salesmans_leads.setdefault(lead.partner_assigned_id.user_id.id, []).append(lead.id)