[FW][FIX] crm: avoid spurious date_open/last_stage_update

Before this commit:
  * when a user changes 'user_id' to set the same previous 'user_id', the
    assign date 'date_open' is updated but it should not as the responsible
    did not change;
  * when a user changes the salesperson 'user_id' of a crm lead, it triggers
    a recompute of 'team_id' that triggers a recompute of 'stage_id' that
    updates  'date_last_stage_update' even if the stage does not change, which
    happens frequently when changing leads within a given team (new assign,
    salesperson on holidays, ...)
  * when merging opportunities, 'user_id' can be set on the main opportunity
    which triggers a recomputation of both 'date_last_stage_update' and
    'date_open' as explained in above points;

Reason:
The 'date_last_stage_update' field depends on 'stage_id' which depends on
'team_id' which depends on 'user_id'. As a result, when 'user_id' changes,
'date_last_stage_update' also updates.

Moreover those fields are implemented using editable stored computed fields
which are triggered everytime a value is given to those fields, even when
the same value is given.

After this commit:
'date_last_stage_update' and 'date_open' will only update when there are
real changes.

Also fix 'date_open' update when lead is converted into an opportunity.
'date_open' is the date when a user is assigned to a lead / opportunity. It
should not be set when converting a lead to an opportunity, as those two
flows are different. Only setting a responsible should update it.

Task-3515225

closes odoo/odoo#161918

Forward-port-of: odoo/odoo#144848
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2024-04-16 07:02:57 +00:00
committed by Odoo Online
3 changed files with 162 additions and 17 deletions
+23 -2
View File
@@ -134,6 +134,7 @@
<field name="priority">2</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(weeks=2)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="stage_lead1"/>
<field name="description"></field>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
@@ -173,6 +174,7 @@ Contact: +1 813 494 5005</p>]]></field>
<field name="priority">2</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(weeks=2)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="stage_lead1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_services"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
@@ -195,6 +197,7 @@ Contact: +1 813 494 5005</p>]]></field>
<field name="priority">0</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(weeks=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="stage_lead1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
@@ -216,6 +219,7 @@ Contact: +1 813 494 5005</p>]]></field>
<field name="priority">1</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="stage_lead1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
@@ -251,6 +255,7 @@ ESM Expert<br />]]></field>
<field name="priority">2</field>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=4)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="stage_lead1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
@@ -272,6 +277,7 @@ ESM Expert<br />]]></field>
<field name="priority">2</field>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="stage_lead1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
@@ -307,6 +313,7 @@ Andrew</p>]]></field>
<field name="priority">2</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="stage_lead1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
@@ -356,6 +363,7 @@ Andrew</p>]]></field>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(weeks=3)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
@@ -382,6 +390,7 @@ Andrew</p>]]></field>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(weeks=2)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_website"/>
@@ -405,6 +414,7 @@ Andrew</p>]]></field>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(weeks=2)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead2"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_services"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
@@ -494,6 +504,7 @@ Andrew</p>]]></field>
<field name="date_deadline" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(weeks=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead2"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
@@ -519,6 +530,7 @@ Andrew</p>]]></field>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(weeks=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead2"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_website"/>
@@ -552,6 +564,7 @@ Andrew</p>]]></field>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(months=2)).strftime('%Y-%m-%d %H:%M')"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(weeks=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead2"/>
<field eval="1" name="active"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_services"/>
@@ -602,6 +615,7 @@ Andrew</p>]]></field>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(weeks=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
@@ -626,6 +640,7 @@ Andrew</p>]]></field>
<field name="date_deadline" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_services"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
@@ -646,6 +661,7 @@ Andrew</p>]]></field>
<field name="priority">2</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_services"/>
<field name="medium_id" ref="utm.utm_medium_phone"/>
@@ -665,6 +681,7 @@ Andrew</p>]]></field>
<field name="priority">1</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
@@ -683,6 +700,7 @@ Andrew</p>]]></field>
<field name="date_deadline" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field name="campaign_id" ref="utm.utm_campaign_fall_drive"/>
<field name="medium_id" ref="utm.utm_medium_direct"/>
@@ -703,6 +721,7 @@ Andrew</p>]]></field>
<field name="tag_ids" eval="[(6, 0, [ref('sales_team.categ_oppor7')])]"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_website"/>
@@ -727,6 +746,7 @@ Andrew</p>]]></field>
<field name="tag_ids" eval="[(6, 0, [ref('sales_team.categ_oppor4')])]"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_services"/>
<field name="medium_id" ref="utm.utm_medium_phone"/>
@@ -750,6 +770,7 @@ Andrew</p>]]></field>
<field name="tag_ids" eval="[(6, 0, [ref('sales_team.categ_oppor2')])]"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="date_open" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_website"/>
@@ -816,7 +837,7 @@ Andrew</p>]]></field>
<field name="phone">+33 1 25 54 45 69</field>
<field name="tag_ids" eval="[(6, 0, [ref('sales_team.categ_oppor6')])]"/>
<field name="priority">1</field>
<field name="date_open" eval="(DateTime.today() - relativedelta(months=2)).strftime('%Y-%m-%d %H:%M')"/>
<field name="date_open" eval="False"/>
<field name="team_id" eval="False"/>
<field name="user_id" eval="False"/>
<field name="stage_id" ref="stage_lead2"/>
@@ -840,7 +861,7 @@ Andrew</p>]]></field>
<field name="phone">+32 22 33 54 07</field>
<field name="tag_ids" eval="[(6, 0, [ref('sales_team.categ_oppor6')])]"/>
<field name="priority">1</field>
<field name="date_open" eval="(DateTime.today() - relativedelta(months=2)).strftime('%Y-%m-%d %H:%M')"/>
<field name="date_open" eval="False"/>
<field name="team_id" eval="False"/>
<field name="user_id" eval="False"/>
<field name="stage_id" ref="stage_lead1"/>
+33 -12
View File
@@ -297,7 +297,8 @@ class Lead(models.Model):
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
if lead.team_id != team:
lead.team_id = team.id
@api.depends('user_id', 'team_id', 'partner_id')
def _compute_company_id(self):
@@ -345,12 +346,14 @@ class Lead(models.Model):
@api.depends('user_id')
def _compute_date_open(self):
for lead in self:
lead.date_open = self.env.cr.now() if lead.user_id else False
if not lead.date_open and lead.user_id:
lead.date_open = self.env.cr.now()
@api.depends('stage_id')
def _compute_date_last_stage_update(self):
for lead in self:
lead.date_last_stage_update = self.env.cr.now()
if not lead.date_last_stage_update:
lead.date_last_stage_update = self.env.cr.now()
@api.depends('create_date', 'date_open')
def _compute_day_open(self):
@@ -767,13 +770,27 @@ class Lead(models.Model):
if vals.get('website'):
vals['website'] = self.env['res.partner']._clean_website(vals['website'])
stage_updated, stage_is_won = vals.get('stage_id'), False
# stage change: update date_last_stage_update
if stage_updated:
stage = self.env['crm.stage'].browse(vals['stage_id'])
if stage.is_won:
vals.update({'probability': 100, 'automated_probability': 100})
stage_is_won = True
now = self.env.cr.now()
stage_updated, stage_is_won = False, False
# stage change (or reset): update date_last_stage_update if at least one
# lead does not have the same stage
if 'stage_id' in vals:
stage_updated = any(lead.stage_id.id != vals['stage_id'] for lead in self)
if stage_updated:
vals['date_last_stage_update'] = now
if stage_updated and vals.get('stage_id'):
stage = self.env['crm.stage'].browse(vals['stage_id'])
if stage.is_won:
vals.update({'probability': 100, 'automated_probability': 100})
stage_is_won = True
# user change; update date_open if at least one lead does not
# have the same user
if 'user_id' in vals and not vals.get('user_id'):
vals['date_open'] = False
elif vals.get('user_id'):
user_updated = any(lead.user_id.id != vals['user_id'] for lead in self)
if user_updated:
vals['date_open'] = now
# stage change with new stage: update probability and date_closed
if vals.get('probability', 0) >= 100 or not vals.get('active', True):
@@ -1432,7 +1449,12 @@ class Lead(models.Model):
if merged_data.get('stage_id') not in team_stage_ids.ids:
merged_data['stage_id'] = team_stage_ids[0].id if team_stage_ids else False
# write merged data into first opportunity
# write merged data into first opportunity; remove some keys if already
# set on opp to avoid useless recomputes
if 'user_id' in merged_data and opportunities_head.user_id.id == merged_data['user_id']:
merged_data.pop('user_id')
if 'team_id' in merged_data and opportunities_head.team_id.id == merged_data['team_id']:
merged_data.pop('team_id')
opportunities_head.write(merged_data)
# delete tail opportunities
@@ -1686,7 +1708,6 @@ class Lead(models.Model):
new_team_id = team_id if team_id else self.team_id.id
upd_values = {
'type': 'opportunity',
'date_open': self.env.cr.now(),
'date_conversion': self.env.cr.now(),
}
if customer != self.partner_id:
+106 -3
View File
@@ -3,6 +3,7 @@
from datetime import datetime
from freezegun import freeze_time
from unittest.mock import patch
from odoo import fields
from odoo.addons.base.tests.test_format_address_mixin import FormatAddressCase
@@ -561,11 +562,23 @@ class TestCRMLead(TestCrmCommon):
@users('user_sales_manager')
def test_crm_lead_stages(self):
lead = self.lead_1.with_user(self.env.user)
self.assertEqual(lead.team_id, self.sales_team_1)
first_now = datetime(2023, 11, 6, 8, 0, 0)
with patch.object(self.env.cr, 'now', lambda: first_now), \
freeze_time(first_now):
self.lead_1.write({'date_open': first_now})
lead.convert_opportunity(self.contact_1)
lead = self.lead_1.with_user(self.env.user)
self.assertEqual(lead.date_open, first_now)
self.assertEqual(lead.team_id, self.sales_team_1)
self.assertEqual(lead.user_id, self.user_sales_leads)
second_now = datetime(2023, 11, 8, 8, 0, 0)
with patch.object(self.env.cr, 'now', lambda: second_now), \
freeze_time(second_now):
lead.convert_opportunity(self.contact_1)
self.assertEqual(lead.date_open, first_now)
self.assertEqual(lead.team_id, self.sales_team_1)
self.assertEqual(lead.user_id, self.user_sales_leads)
lead.action_set_won()
self.assertEqual(lead.probability, 100.0)
@@ -636,6 +649,96 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(self.contact_company_1.email, 'broken')
self.assertEqual(self.contact_company_1.phone, 'alsobroken')
@users('user_sales_manager')
def test_crm_lead_update_dates(self):
""" Test date_open / date_last_stage_update update, check those dates
are not erased too often """
first_now = datetime(2023, 11, 6, 8, 0, 0)
with patch.object(self.env.cr, 'now', lambda: first_now), \
freeze_time(first_now):
leads = self.env['crm.lead'].create([
{
'email_from': 'testlead@customer.company.com',
'name': 'Lead_1',
'team_id': self.sales_team_1.id,
'type': 'lead',
'user_id': False,
}, {
'email_from': 'testopp@customer.company.com',
'name': 'Opp_1',
'type': 'opportunity',
'user_id': self.user_sales_salesman.id,
},
])
leads.flush_recordset()
for lead in leads:
self.assertEqual(lead.date_last_stage_update, first_now,
"Stage updated at create time with default value")
self.assertEqual(lead.stage_id, self.stage_team1_1)
self.assertEqual(lead.team_id, self.sales_team_1)
self.assertFalse(leads[0].date_open, "No user -> no assign date")
self.assertFalse(leads[0].user_id)
self.assertEqual(leads[1].date_open, first_now, "Default user assigned")
self.assertEqual(leads[1].user_id, self.user_sales_salesman, "Default user assigned")
# changing user_id may change team_id / stage_id; update date_open and
# maybe date_last_stage_update
updated_time = datetime(2023, 11, 23, 8, 0, 0)
with patch.object(self.env.cr, 'now', lambda: updated_time), \
freeze_time(updated_time):
leads.write({"user_id": self.user_sales_salesman.id})
leads.flush_recordset()
for lead in leads:
self.assertEqual(lead.stage_id, self.stage_team1_1)
self.assertEqual(lead.team_id, self.sales_team_1)
self.assertEqual(
leads[0].date_last_stage_update, first_now,
'Setting same stage when changing user_id, should not update')
self.assertEqual(
leads[0].date_open, updated_time,
'User assigned -> assign date updated')
self.assertEqual(
leads[1].date_last_stage_update, first_now,
'Setting same stage when changing user_id, should not update')
self.assertEqual(
leads[1].date_open, updated_time,
'Should not update date_open, was already the same user_id, but done in batch so ...')
# set won changes stage -> update date_last_stage_update
newer_time = datetime(2023, 11, 26, 8, 0, 0)
with patch.object(self.env.cr, 'now', lambda: newer_time), \
freeze_time(newer_time):
leads[1].action_set_won()
leads[1].flush_recordset()
self.assertEqual(
leads[1].date_last_stage_update, newer_time,
'Mark as won updates stage hence stage update date')
self.assertEqual(leads[1].stage_id, self.stage_gen_won)
# merge may change user_id and then may change team_id / stage_id; in this
# case no real value change is happening
last_time = datetime(2023, 11, 29, 8, 0, 0)
with patch.object(self.env.cr, 'now', lambda: last_time), \
freeze_time(last_time):
leads.merge_opportunity(
user_id=self.user_sales_salesman.id,
auto_unlink=False,
)
leads.flush_recordset()
self.assertEqual(leads[0].date_last_stage_update, first_now)
self.assertEqual(leads[0].date_open, updated_time)
self.assertEqual(leads[0].stage_id, self.stage_team1_1)
self.assertEqual(leads[0].team_id, self.sales_team_1)
self.assertEqual(
leads[1].date_last_stage_update, newer_time,
'Should not rewrite when setting same stage')
self.assertEqual(
leads[1].date_open, updated_time,
'Should not rewrite when setting same user_id')
self.assertEqual(leads[1].stage_id, self.stage_gen_won)
self.assertEqual(leads[1].team_id, self.sales_team_1)
self.assertEqual(leads[1].user_id, self.user_sales_salesman)
@users('user_sales_manager')
def test_crm_team_alias(self):
new_team = self.env['crm.team'].create({