[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
This commit is contained in:
David Beguin
2019-06-11 07:04:33 +00:00
parent f60825b439
commit 8e540558ee
9 changed files with 26 additions and 79 deletions
+1 -8
View File
@@ -2,27 +2,20 @@
<odoo noupdate="1">
<record model="crm.stage" id="stage_lead1">
<field name="name">New</field>
<field name="probability">10</field>
<field name="on_change">1</field>
<field name="sequence">1</field>
</record>
<record model="crm.stage" id="stage_lead2">
<field name="name">Qualified</field>
<field name="probability">30</field>
<field name="on_change">1</field>
<field name="sequence">2</field>
</record>
<record model="crm.stage" id="stage_lead3">
<field name="name">Proposition</field>
<field name="probability">70</field>
<field name="on_change">1</field>
<field name="sequence">3</field>
</record>
<record model="crm.stage" id="stage_lead4">
<field name="name">Won</field>
<field name="fold" eval="False"/>
<field name="probability">100</field>
<field name="on_change">1</field>
<field name="is_won">True</field>
<field name="sequence">70</field>
</record>
</odoo>
+13 -55
View File
@@ -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')
+2 -3
View File
@@ -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
@@ -52,7 +52,7 @@
<filter name="opportunity" string="Opportunities" domain="[('lead_type','=','opportunity')]" help="Show only opportunity" groups="crm.group_use_lead"/>
<separator/>
<filter string="Won" name="won"
domain="['&amp;', ('stage_id.probability', '=', 100), ('stage_id.on_change', '=', 1)]"/>
domain="['&amp;', ('stage_id.is_won', '=', True)]"/>
<separator/>
<filter string="Completion Date" name="completion_date" date="date" default_period="last_365_days"/>
<group expand="1" string="Group By">
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -753,7 +753,7 @@
<filter string="Expected Closing" name="close_this_month" date="date_deadline"/>
<filter string="Closed Date" name="close_date" date="date_closed"/>
<separator/>
<filter string="Won" name="won" domain="['&amp;', ('active', '=', True), ('stage_id.probability', '=', 100)]"/>
<filter string="Won" name="won" domain="['&amp;', ('active', '=', True), ('stage_id.is_won', '=', True)]"/>
<filter string="Lost" name="lost" domain="['&amp;', ('active', '=', False), ('probability', '=', 0)]"/>
<separator/>
<filter string="Activities Todo" name="activities_my"
+4 -7
View File
@@ -8,7 +8,7 @@
<search string="Stage Search">
<field name="name"/>
<field name="sequence"/>
<field name="probability"/>
<field name="is_won"/>
<field name="team_id"/>
</search>
</field>
@@ -21,7 +21,7 @@
<tree string="Stages">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="probability"/>
<field name="is_won"/>
<field name="team_id"/>
</tree>
</field>
@@ -44,14 +44,11 @@
</div>
<group>
<group>
<field name="on_change"/>
<field name="probability" attrs="{'invisible': [('on_change', '=', False)]}"/>
</group>
<field name="team_count" invisible="1"/>
<group>
<field name="is_won"/>
<field name="fold"/>
<field name="team_id" options='{"no_open": True, "no_create": True}' attrs="{'invisible': [('team_count', '&lt;=', 1)]}"/>
</group>
<field name="team_count" invisible="1"/>
</group>
<separator string="Requirements"/>
<field name="requirements" nolabel="1" placeholder="Give your team the requirements to move an opportunity to this stage."/>
@@ -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([]),
@@ -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)