diff --git a/addons/crm/data/crm_lead_demo.xml b/addons/crm/data/crm_lead_demo.xml index 8ef7a215796..1c49f1180b1 100644 --- a/addons/crm/data/crm_lead_demo.xml +++ b/addons/crm/data/crm_lead_demo.xml @@ -134,6 +134,7 @@ 2 + @@ -173,6 +174,7 @@ Contact: +1 813 494 5005

]]>
2 + @@ -195,6 +197,7 @@ Contact: +1 813 494 5005

]]>
0 + @@ -216,6 +219,7 @@ Contact: +1 813 494 5005

]]>
1 + @@ -251,6 +255,7 @@ ESM Expert
]]>
2 + @@ -272,6 +277,7 @@ ESM Expert
]]>
2 + @@ -307,6 +313,7 @@ Andrew

]]>
2 + @@ -356,6 +363,7 @@ Andrew

]]>
+ @@ -382,6 +390,7 @@ Andrew

]]>
+ @@ -405,6 +414,7 @@ Andrew

]]>
+ @@ -494,6 +504,7 @@ Andrew

]]>
+ @@ -519,6 +530,7 @@ Andrew

]]>
+ @@ -552,6 +564,7 @@ Andrew

]]>
+ @@ -602,6 +615,7 @@ Andrew

]]>
+ @@ -626,6 +640,7 @@ Andrew

]]>
+ @@ -646,6 +661,7 @@ Andrew

]]>
2 + @@ -665,6 +681,7 @@ Andrew

]]>
1 + @@ -683,6 +700,7 @@ Andrew

]]>
+ @@ -703,6 +721,7 @@ Andrew

]]>
+ @@ -727,6 +746,7 @@ Andrew

]]>
+ @@ -750,6 +770,7 @@ Andrew

]]>
+ @@ -816,7 +837,7 @@ Andrew

]]>
+33 1 25 54 45 69 1 - + @@ -840,7 +861,7 @@ Andrew

]]>
+32 22 33 54 07 1 - + diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 068f1623bdd..4d11d354b29 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -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: diff --git a/addons/crm/tests/test_crm_lead.py b/addons/crm/tests/test_crm_lead.py index 3e975ffb5bc..6627838f352 100644 --- a/addons/crm/tests/test_crm_lead.py +++ b/addons/crm/tests/test_crm_lead.py @@ -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({