diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index abe9e285c82..c2a2bb40759 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -105,17 +105,15 @@ class Lead(models.Model):
date_conversion = fields.Datetime('Conversion Date', readonly=True)
# Probability - Only used for type opportunity
- probability = fields.Float('Probability', group_operator="avg", default=10.0)
+ probability = fields.Float('Probability', group_operator="avg")
automated_probability = fields.Float('Automated Probability', readonly=True)
is_automated_probability = fields.Boolean('Is automated probability?', compute="_compute_is_automated_probability")
phone_state = fields.Selection([
('correct', 'Correct'),
- ('incorrect', 'Incorrect'),
- ('empty', 'Empty')], string='Phone Quality', default="empty", compute="_compute_phone_state", store=True)
+ ('incorrect', 'Incorrect')], string='Phone Quality', compute="_compute_phone_state", store=True)
email_state = fields.Selection([
('correct', 'Correct'),
- ('incorrect', 'Incorrect'),
- ('empty', 'Empty')], string='Email Quality', default="empty", compute="_compute_email_state", store=True)
+ ('incorrect', 'Incorrect')], string='Email Quality', compute="_compute_email_state", store=True)
# Only used for type opportunity
planned_revenue = fields.Monetary('Expected Revenue', currency_field='company_currency', tracking=True)
@@ -222,7 +220,7 @@ class Lead(models.Model):
@api.depends('phone', 'country_id.code')
def _compute_phone_state(self):
for lead in self:
- phone_status = 'empty'
+ phone_status = False
if lead.phone:
country_code = lead.country_id.code if lead.country_id and lead.country_id.code else None
try:
@@ -235,11 +233,11 @@ class Lead(models.Model):
@api.depends('email_from')
def _compute_email_state(self):
for lead in self:
- email_state = 'empty'
+ email_state = False
if lead.email_from:
email_state = 'incorrect'
for email in email_split(lead.email_from):
- if tools.email_validate(email):
+ if tools.email_normalize(email):
email_state = 'correct'
break
lead.email_state = email_state
@@ -483,9 +481,24 @@ class Lead(models.Model):
# Actions Methods
# ----------------------------------------
+ def _rebuild_pls_frequency_table_threshold(self):
+ """ Called by action_set_lost and action_set_won.
+ Will run the cron to update the frequency table only if the number of lead is above
+ a specified value (from config_parameter) for onboarding purpose.
+ Once the threshold is reached, the config_param is set to 0 to avoid re-run the cron
+ and, mainly, to avoid making useless search_count in the future."""
+ pls_threshold = int(self.env['ir.config_parameter'].sudo().get_param('crm.pls_rebuild_threshold'))
+ if pls_threshold:
+ lead_count = self.env['crm.lead'].sudo().search_count([])
+ if lead_count < pls_threshold:
+ self.sudo()._cron_update_automated_probabilities()
+ else:
+ self.env['ir.config_parameter'].sudo().set_param('crm.pls_rebuild_threshold', 0)
+
def action_set_lost(self, **additional_values):
""" Lost semantic: probability = 0 or active = False """
result = self.write({'active': False, 'probability': 0, **additional_values})
+ self._rebuild_pls_frequency_table_threshold()
return result
def action_set_won(self):
@@ -493,6 +506,7 @@ class Lead(models.Model):
for lead in self:
stage_id = lead._stage_find(domain=[('is_won', '=', True)])
lead.write({'stage_id': stage_id.id, 'probability': 100})
+ self._rebuild_pls_frequency_table_threshold()
return True
def action_set_automated_probability(self):
@@ -1320,6 +1334,7 @@ class Lead(models.Model):
# get stages
first_stage_id = self.env['crm.stage'].search([], order='sequence', limit=1)
+ won_stage_ids = self.env['crm.stage'].search([('is_won', '=', True)]).ids
# Get all leads values, no matter the team_id
leads_values_dict = self._pls_get_lead_pls_values(batch_mode=batch_mode)
@@ -1328,8 +1343,11 @@ class Lead(models.Model):
# Get unique couples to search in frequency table
leads_values = set()
- for values in leads_values_dict.values():
+ won_leads = set()
+ for lead_id, values in leads_values_dict.items():
for couple in values['values']:
+ if couple[0] == 'stage_id' and couple[1] in won_stage_ids:
+ won_leads.add(lead_id)
leads_values.add(couple)
# get all variable related records from frequency table, no matter the team_id
@@ -1374,6 +1392,16 @@ class Lead(models.Model):
save_team_id = None
p_won, p_lost = 1, 1
for lead_id, lead_values in leads_values_dict.items():
+ # if stage_id is null, return 0 and bypass computation
+ lead_fields = [value[0] for value in lead_values.get('values', [])]
+ if not 'stage_id' in lead_fields:
+ lead_probabilities[lead_id] = 0
+ continue
+ # if lead stage is won, return 100
+ elif lead_id in won_leads:
+ lead_probabilities[lead_id] = 100
+ continue
+
lead_team_id = lead_values['team_id'] if lead_values['team_id'] else 0 # team_id = None -> Convert to 0
lead_team_id = lead_team_id if lead_team_id in result else -1 # team_id not in frequency Table -> convert to -1
if lead_team_id != save_team_id:
@@ -1428,15 +1456,24 @@ class Lead(models.Model):
# Recompute all the leads. Won : probability = 100 | Lost : probability = 0 or inactive
# Here, inactive won't be returned anyway
+ # Get also all the lead without probability --> These are the new leads. Activate auto probability on them.
pls_start_date = self._pls_get_safe_start_date()
if pls_start_date:
- pending_lead_domain = [('probability', '<', 100), ('probability', '>', 0), ('create_date', '>', pls_start_date)]
+ pending_lead_domain = ['&', '&', ('stage_id', '!=', False), ('create_date', '>', pls_start_date),
+ '|', ('probability', '=', False), '&', ('probability', '<', 100), ('probability', '>', 0)]
leads_to_update = self.env['crm.lead'].search(pending_lead_domain)
lead_probabilities = leads_to_update._pls_get_naive_bayes_probabilities(batch_mode=True)
# Update in execute batch to avoid server roundtrips + page_size to 10000 to avoid memory errors
- sql = "update crm_lead set automated_probability = %s where id = %s"
- batch_params = [(lead_probabilities[lead.id], lead.id) for lead in leads_to_update if lead.id in lead_probabilities]
+ # Update both probability and automated_probability if they were equal, else, update only automated_probability
+ sql = """UPDATE crm_lead
+ SET automated_probability = %s,
+ probability = CASE WHEN (ROUND(probability::numeric, 2) = ROUND(automated_probability::numeric, 2) or probability is null)
+ THEN (%s)
+ ELSE (probability)
+ END
+ WHERE id = %s"""
+ batch_params = [(lead_probabilities[lead.id], lead_probabilities[lead.id], lead.id) for lead in leads_to_update if lead.id in lead_probabilities]
extras.execute_batch(self._cr, sql, batch_params, page_size=10000)
_logger.info("Predictive Lead Scoring : all automated probability updated (count: %d)" % (len(leads_to_update)))
@@ -1464,7 +1501,7 @@ class Lead(models.Model):
that are defined on the model. """
pls_fields_config = self.env['ir.config_parameter'].sudo().get_param('crm.pls_fields')
pls_fields = pls_fields_config.split(',') if pls_fields_config else []
- pls_safe_fields = [field for field in pls_fields if field in self._fields]
+ pls_safe_fields = [field for field in pls_fields if field in self._fields.keys()]
return pls_safe_fields
# Rebuild Frequency Table Tools
@@ -1525,7 +1562,7 @@ class Lead(models.Model):
lost = result['count'] if result['probability'] == 0 else 0
for field in fields:
value = result[field]
- if value:
+ if value or field in ('email_state', 'phone_state'):
if field == 'stage_id':
if won: # increment all stages if won
stages_to_increment = [stage['id'] for stage in stage_ids]
@@ -1600,7 +1637,7 @@ class Lead(models.Model):
self.flush(['probability'])
query = """SELECT id, %s
FROM crm_lead l
- WHERE probability > 0 AND probability < 100 AND active = True AND id in %%s order by team_id asc"""
+ WHERE ((probability > 0 AND probability < 100) OR probability is null) AND active = True AND id in %%s order by team_id asc"""
query = sql.SQL(query % str_fields).format(*args)
self._cr.execute(query, [tuple(self.ids)])
@@ -1610,7 +1647,7 @@ class Lead(models.Model):
FROM crm_lead l
LEFT JOIN crm_lead_tag_rel rel ON l.id = rel.lead_id
LEFT JOIN crm_lead_tag t ON rel.tag_id = t.id
- WHERE l.probability > 0 AND l.probability < 100 AND l.active = True AND l.id in %s order by l.team_id asc"""
+ WHERE ((l.probability > 0 AND l.probability < 100) OR l.probability is null) AND l.active = True AND l.id in %s order by l.team_id asc"""
self._cr.execute(query, [tuple(self.ids)])
tag_results = self._cr.dictfetchall()
@@ -1621,7 +1658,7 @@ class Lead(models.Model):
if field == 'team_id': # ignore team_id as stored separately in leads_values_dict[lead_id][team_id]
continue
value = lead[field]
- if value:
+ if value or field in ('email_state', 'phone_state'):
lead_values.append((field, value))
leads_values_dict[lead['id']] = {'values': lead_values, 'team_id': lead['team_id']}
@@ -1636,7 +1673,7 @@ class Lead(models.Model):
if field == 'team_id': # ignore team_id as stored separately in leads_values_dict[lead_id][team_id]
continue
value = lead[field].id if isinstance(lead[field], models.BaseModel) else lead[field]
- if value:
+ if value or field in ('email_state', 'phone_state'):
lead_values.append((field, value))
for tag in lead.tag_ids:
lead_values.append(('tag_id', tag.id))
@@ -1652,7 +1689,7 @@ class Lead(models.Model):
:param first_stage_id: stage with smallest sequence
:return: won count, lost count and total count for all records in frequencies
"""
- if str(first_stage_id.id) not in team_results['stage_id']:
+ if str(first_stage_id.id) not in team_results.get('stage_id', []):
return 0, 0, 0
stage_result = team_results['stage_id'][str(first_stage_id.id)]
return stage_result['won'], stage_result['lost'], stage_result['won'] + stage_result['lost']
diff --git a/addons/crm/models/res_config_settings.py b/addons/crm/models/res_config_settings.py
index deb4fc322a9..07b91adef98 100644
--- a/addons/crm/models/res_config_settings.py
+++ b/addons/crm/models/res_config_settings.py
@@ -16,7 +16,7 @@ class ResConfigSettings(models.TransientModel):
module_crm_iap_lead_enrich = fields.Boolean("Enrich your leads automatically with company data based on their email address.")
lead_mining_in_pipeline = fields.Boolean("Create a lead mining request directly from the opportunity pipeline.", config_parameter='crm.lead_mining_in_pipeline')
predictive_lead_scoring_start_date = fields.Date(string='Lead Scoring Starting Date', compute="_compute_pls_start_date", inverse="_inverse_pls_start_date_str")
- predictive_lead_scoring_start_date_str = fields.Char(string='Lead Scoring Starting Date in String', default=date.today().strftime('%Y-%m-%d'), config_parameter='crm.pls_start_date')
+ predictive_lead_scoring_start_date_str = fields.Char(string='Lead Scoring Starting Date in String', config_parameter='crm.pls_start_date')
predictive_lead_scoring_fields = fields.Many2many('crm.lead.scoring.frequency.field', string='Lead Scoring Frequency Fields', compute="_compute_pls_fields", inverse="_inverse_pls_fields_str")
predictive_lead_scoring_fields_str = fields.Char(string='Lead Scoring Frequency Fields in String', config_parameter='crm.pls_fields')
@@ -39,7 +39,8 @@ class ResConfigSettings(models.TransientModel):
for setting in self:
if setting.predictive_lead_scoring_fields_str:
names = setting.predictive_lead_scoring_fields_str.split(',')
- setting.predictive_lead_scoring_fields = self.env['crm.lead.scoring.frequency.field'].search([('name', 'in', names)])
+ fields = self.env['ir.model.fields'].search([('name', 'in', names), ('model', '=', 'crm.lead')])
+ setting.predictive_lead_scoring_fields = self.env['crm.lead.scoring.frequency.field'].search([('field_id', 'in', fields.ids)])
else:
setting.predictive_lead_scoring_fields = None
@@ -48,7 +49,7 @@ class ResConfigSettings(models.TransientModel):
we store the fields with a comma separated string into a Char config field """
for setting in self:
if setting.predictive_lead_scoring_fields:
- setting.predictive_lead_scoring_fields_str = ','.join(setting.predictive_lead_scoring_fields.mapped('name'))
+ setting.predictive_lead_scoring_fields_str = ','.join(setting.predictive_lead_scoring_fields.mapped('field_id.name'))
else:
setting.predictive_lead_scoring_fields_str = ''
diff --git a/addons/crm/views/res_config_settings_views.xml b/addons/crm/views/res_config_settings_views.xml
index baa8dc3d925..4431b46aae6 100644
--- a/addons/crm/views/res_config_settings_views.xml
+++ b/addons/crm/views/res_config_settings_views.xml
@@ -66,7 +66,7 @@
Consider leads created as of the
-
+
diff --git a/addons/crm_iap_lead_enrich/models/crm_lead.py b/addons/crm_iap_lead_enrich/models/crm_lead.py
index 48df18f3c77..dd6e4b9ee1d 100644
--- a/addons/crm_iap_lead_enrich/models/crm_lead.py
+++ b/addons/crm_iap_lead_enrich/models/crm_lead.py
@@ -19,10 +19,11 @@ class Lead(models.Model):
@api.model
def _iap_enrich_leads_cron(self):
timeDelta = fields.datetime.now() - datetime.timedelta(hours=1)
+ # Get all leads not lost nor won (lost: active = False)
leads = self.search([
('iap_enrich_done', '=', False),
('reveal_id', '=', False),
- ('probability', 'not in', (0, 100)),
+ ('probability', '<', 100),
('create_date', '>', timeDelta)
])
leads._iap_enrich(from_cron=True)
@@ -30,7 +31,8 @@ class Lead(models.Model):
def _iap_enrich(self, from_cron=False):
lead_emails = {}
for lead in self:
- if lead.probability in (0, 100) or lead.iap_enrich_done:
+ # If lead is lost, active == False, but is anyway removed from the search in the cron.
+ if lead.probability == 100 or lead.iap_enrich_done:
continue
normalized_email = tools.email_normalize(lead.partner_address_email) or tools.email_normalize(lead.email_from)
if normalized_email:
diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py
index 50ccd6b0375..f9a5cfa67cf 100644
--- a/odoo/tools/mail.py
+++ b/odoo/tools/mail.py
@@ -20,31 +20,6 @@ from odoo.tools import misc
_logger = logging.getLogger(__name__)
-# Optional Flanker dependency for email validation.
-_flanker_warning = False
-try:
- from flanker.addresslib import address
- if hasattr(address, 'six'):
- # Python 3 supported
- def checkmail(mail):
- return bool(address.validate_address(mail))
- else:
- def checkmail(mail):
- global _flanker_warning
- if not _flanker_warning:
- _logger.info("Flanker version 0.9 or greater required for Python 3 compatibility")
- _flanker_warning = True
- return True
-
-except ImportError:
- _logger.info('The flanker Python module is not installed, so email validation with flanker is unavailable')
- def checkmail(mail):
- global _flanker_warning
- if not _flanker_warning:
- _logger.info('The flanker Python module is not installed, so email validation with flanker is unavailable')
- _flanker_warning = True
- return True
-
#----------------------------------------------------------
# HTML Sanitizer
#----------------------------------------------------------
@@ -531,16 +506,6 @@ def email_normalize(text):
return False
return emails[0].lower()
-def email_validate(email):
- """
- Check is the email is valid using email normalization + optional Flanker module.
- If Flanker is installed, it will check if email is valid (checking DNS and other stuff).
- If Flanker is not installed, this method only normalizes the email
- :param text: string containing the email to validate
- :return: normalized email if mail is valid or False
- """
- return email_normalize(email) if checkmail(email) else False
-
def email_escape_char(email_address):
""" Escape problematic characters in the given email address string"""
return email_address.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')