19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:53 +01:00
parent dc68f80d3f
commit 7221b9ac46
610 changed files with 135477 additions and 161677 deletions

View file

@ -5,12 +5,14 @@ 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
from odoo.addons.crm.models.crm_lead import PARTNER_FIELDS_TO_SYNC, PARTNER_ADDRESS_FIELDS_TO_SYNC
from odoo.addons.crm.tests.common import TestCrmCommon, INCOMING_EMAIL
from odoo.addons.mail.tests.common_tracking import MailTrackingDurationMixinCase
from odoo.addons.phone_validation.tools.phone_validation import phone_format
from odoo.exceptions import UserError
from odoo.tests.common import Form, tagged, users
from odoo.tests import Form, tagged, users
from odoo.tools import mute_logger
@ -76,6 +78,37 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(self.contact_company_1.lang, self.lang_en.code)
self.assertEqual(lead.lang_id, self.lang_en)
@users('user_sales_leads')
def test_crm_lead_compute_commercial_partner(self):
company_partner, child_partner, orphan_partner = self.env['res.partner'].create([
{
'name': 'test_crm_lead_compute_commercial_partner',
'is_company': True,
'email': 'test_crm_lead_compute_commercial_partner@test.lan',
},
{'name': 'Test Child'},
{'name': 'Test Orphan'},
])
child_partner.parent_id = company_partner
lead = self.env['crm.lead'].create({
'name': 'Test Lead',
'partner_name': 'test_crm_lead_compute_commercial_partner',
})
self.assertEqual(lead.commercial_partner_id, company_partner)
lead.partner_id = orphan_partner
self.assertFalse(lead.commercial_partner_id)
lead.partner_id = child_partner
self.assertEqual(lead.commercial_partner_id, company_partner)
lead.write({
'partner_id': False,
'partner_name': False,
})
self.assertFalse(lead.commercial_partner_id)
lead.partner_id = company_partner
# this is mostly because we use it to set "parent_id" in most flows
# and it doesn't really make sense to have it be its own parent
self.assertFalse(lead.commercial_partner_id, "If a partner is its own commercial_partner_id, the lead is considered to have none.")
@users('user_sales_leads')
def test_crm_lead_creation_no_partner(self):
lead_data = {
@ -184,8 +217,7 @@ class TestCRMLead(TestCrmCommon):
'name': 'Empty partner',
'is_company': True,
'lang': 'en_US',
'mobile': '123456789',
'title': self.env.ref('base.res_partner_title_mister').id,
'phone': '0485112233',
'function': 'My function',
})
lead_data = {
@ -195,7 +227,6 @@ class TestCRMLead(TestCrmCommon):
'country_id': self.country_ref.id,
'email_from': self.test_email,
'phone': self.test_phone,
'mobile': '987654321',
'website': 'http://mywebsite.org',
}
lead = self.env['crm.lead'].create(lead_data)
@ -214,8 +245,6 @@ class TestCRMLead(TestCrmCommon):
# PARTNER_FIELDS_TO_SYNC
self.assertEqual(lead.lang_id, self.lang_en)
self.assertEqual(lead.phone, lead_data['phone'], "Phone should keep its initial value")
self.assertEqual(lead.mobile, empty_partner.mobile, "Mobile from partner should be set on the lead")
self.assertEqual(lead.title, empty_partner.title, "Title from partner should be set on the lead")
self.assertEqual(lead.function, empty_partner.function, "Function from partner should be set on the lead")
self.assertEqual(lead.website, lead_data['website'], "Website should keep its initial value")
@ -247,27 +276,43 @@ class TestCRMLead(TestCrmCommon):
@users('user_sales_manager')
def test_crm_lead_currency_sync(self):
lead = self.env['crm.lead'].create({
lead_company = self.env['res.company'].sudo().create({
'name': 'EUR company',
'currency_id': self.env.ref('base.EUR').id,
})
lead = self.env['crm.lead'].with_company(lead_company).create({
'name': 'Lead 1',
'company_id': self.company_main.id
'company_id': lead_company.id
})
self.assertEqual(lead.company_currency, self.env.ref('base.EUR'))
self.company_main.currency_id = self.env.ref('base.CHF')
lead.with_company(self.company_main).update({'company_id': False})
lead_company.currency_id = self.env.ref('base.CHF')
lead.update({'company_id': False})
self.assertEqual(lead.company_currency, self.env.ref('base.CHF'))
#set back original currency
self.company_main.currency_id = self.env.ref('base.EUR')
@users('user_sales_manager')
def test_crm_lead_date_closed(self):
# ensure a lead created directly in a won stage gets a date_closed
lead_in_won = self.env['crm.lead'].create({
'name': 'Created in Won',
'type': 'opportunity',
'stage_id': self.stage_team1_won.id,
'expected_revenue': 123.45,
})
# date_closed must be set at creation when stage is won
self.assertTrue(lead_in_won.date_closed, "Lead created in a won stage must have date_closed set")
self.assertIsInstance(lead_in_won.date_closed, datetime)
# Test for one won lead
stage_team1_won2 = self.env['crm.stage'].create({
'name': 'Won2',
'sequence': 75,
'team_id': self.sales_team_1.id,
'team_ids': [self.sales_team_1.id],
'is_won': True,
})
old_date_closed = lead_in_won.date_closed
with freeze_time('2020-02-02 18:00'):
lead_in_won.stage_id = stage_team1_won2
self.assertEqual(lead_in_won.date_closed, old_date_closed, 'Moving between won stages should not change existing date_closed')
won_lead = self.lead_team_1_won.with_env(self.env)
other_lead = self.lead_1.with_env(self.env)
old_date_closed = won_lead.date_closed
@ -301,6 +346,36 @@ class TestCRMLead(TestCrmCommon):
lead.action_set_lost()
self.assertEqual(lead.date_closed, datetime.now(), "Closed date is updated after marking lead as lost")
@users('user_sales_manager')
def test_crm_lead_meeting_display_fields(self):
lead = self.env['crm.lead'].create({'name': 'Lead With Meetings'})
meeting_1, meeting_2, meeting_3 = self.env['calendar.event'].create([{
'name': 'Meeting 1 of Lead',
'opportunity_id': lead.id,
'start': '2022-07-12 08:00:00',
'stop': '2022-07-12 10:00:00',
}, {
'name': 'Meeting 2 of Lead',
'opportunity_id': lead.id,
'start': '2022-07-14 08:00:00',
'stop': '2022-07-14 10:00:00',
}, {
'name': 'Meeting 3 of Lead',
'opportunity_id': lead.id,
'start': '2022-07-15 08:00:00',
'stop': '2022-07-15 10:00:00',
}])
with freeze_time('2022-07-13 11:00:00'):
self.assertEqual(lead.meeting_display_date, fields.Date.from_string('2022-07-14'))
self.assertEqual(lead.meeting_display_label, 'Next Meeting')
(meeting_2 | meeting_3).unlink()
self.assertEqual(lead.meeting_display_date, fields.Date.from_string('2022-07-12'))
self.assertEqual(lead.meeting_display_label, 'Last Meeting')
meeting_1.unlink()
self.assertFalse(lead.meeting_display_date)
self.assertEqual(lead.meeting_display_label, 'No Meeting')
@users('user_sales_manager')
def test_crm_lead_partner_sync(self):
lead, partner = self.lead_1.with_user(self.env.user), self.contact_2
@ -325,11 +400,13 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(lead.email_from, partner_email)
self.assertEqual(lead.phone, '+1 202 555 6666')
# resetting lead values also resets partner
# resetting lead values should not reset partner: voiding lead info (because
# of some reasons) should not prevent from using the contact in other records
lead.email_from, lead.phone = False, False
self.assertFalse(partner.email)
self.assertFalse(partner.email_normalized)
self.assertFalse(partner.phone)
self.assertFalse(lead.email_from)
self.assertFalse(lead.phone)
self.assertEqual(partner.email, partner_email)
self.assertEqual(partner.phone, '+1 202 555 6666')
@users('user_sales_manager')
def test_crm_lead_partner_sync_email_phone(self):
@ -345,19 +422,14 @@ class TestCRMLead(TestCrmCommon):
lead_form = Form(lead)
# reset partner phone to a local number and prepare formatted / sanitized values
partner_phone, partner_mobile = self.test_phone_data[2], self.test_phone_data[1]
partner_phone = self.test_phone_data[2]
partner_phone_formatted = phone_format(partner_phone, 'US', '1', force_format='INTERNATIONAL')
partner_phone_sanitized = phone_format(partner_phone, 'US', '1', force_format='E164')
partner_mobile_formatted = phone_format(partner_mobile, 'US', '1', force_format='INTERNATIONAL')
partner_mobile_sanitized = phone_format(partner_mobile, 'US', '1', force_format='E164')
partner_email, partner_email_normalized = self.test_email_data[2], self.test_email_data_normalized[2]
self.assertEqual(partner_phone_formatted, '+1 202-555-0888')
self.assertEqual(partner_phone_sanitized, self.test_phone_data_sanitized[2])
self.assertEqual(partner_mobile_formatted, '+1 202-555-0999')
self.assertEqual(partner_mobile_sanitized, self.test_phone_data_sanitized[1])
# ensure initial data
self.assertEqual(partner.phone, partner_phone)
self.assertEqual(partner.mobile, partner_mobile)
self.assertEqual(partner.email, partner_email)
# LEAD/PARTNER SYNC: email and phone are propagated to lead
@ -366,8 +438,6 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(lead_form.email_from, partner_email)
self.assertEqual(lead_form.phone, partner_phone_formatted,
'Lead: form automatically formats numbers')
self.assertEqual(lead_form.mobile, partner_mobile_formatted,
'Lead: form automatically formats numbers')
self.assertFalse(lead_form.partner_email_update)
self.assertFalse(lead_form.partner_phone_update)
@ -380,9 +450,7 @@ class TestCRMLead(TestCrmCommon):
'Lead / Partner: equal emails should lead to equal normalized emails')
self.assertEqual(lead.phone, partner_phone_formatted,
'Lead / Partner: partner values (formatted) sent to lead')
self.assertEqual(lead.mobile, partner_mobile_formatted,
'Lead / Partner: partner values (formatted) sent to lead')
self.assertEqual(lead.phone_sanitized, partner_mobile_sanitized,
self.assertEqual(lead.phone_sanitized, partner_phone_sanitized,
'Lead: phone_sanitized computed field on mobile')
# for email_from, if only formatting differs, warning should not appear and
@ -406,6 +474,7 @@ class TestCRMLead(TestCrmCommon):
self.assertTrue(lead_form.partner_email_update)
new_phone = '+1 202 555 7799'
new_phone_formatted = phone_format(new_phone, 'US', '1', force_format="INTERNATIONAL")
new_phone_sanitized = phone_format(new_phone, 'US', '1', force_format="E164")
lead_form.phone = new_phone
self.assertEqual(lead_form.phone, new_phone_formatted)
self.assertTrue(lead_form.partner_email_update)
@ -416,30 +485,21 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(partner.email_normalized, new_email_normalized)
self.assertEqual(partner.phone, new_phone_formatted)
# LEAD/PARTNER SYNC: mobile does not update partner
new_mobile = '+1 202 555 6543'
new_mobile_formatted = phone_format(new_mobile, 'US', '1', force_format="INTERNATIONAL")
lead_form.mobile = new_mobile
# LEAD/PARTNER SYNC: resetting lead values should not reset partner
# # voiding lead info (because of some reasons) should not prevent
# # from using the contact in other records
lead_form.email_from, lead_form.phone = False, False
self.assertFalse(lead_form.partner_email_update)
self.assertFalse(lead_form.partner_phone_update)
lead_form.save()
self.assertEqual(lead.mobile, new_mobile_formatted)
self.assertEqual(partner.mobile, partner_mobile)
# LEAD/PARTNER SYNC: reseting lead values also resets partner for email
# and phone, but not for mobile
lead_form.email_from, lead_form.phone, lead.mobile = False, False, False
self.assertTrue(lead_form.partner_email_update)
self.assertTrue(lead_form.partner_phone_update)
lead_form.save()
self.assertFalse(partner.email)
self.assertFalse(partner.email_normalized)
self.assertFalse(partner.phone)
self.assertEqual(partner.email, new_email)
self.assertEqual(partner.email_normalized, new_email_normalized)
self.assertEqual(partner.phone, new_phone_formatted)
self.assertFalse(lead.phone)
self.assertFalse(lead.mobile)
self.assertFalse(lead.phone_sanitized)
self.assertEqual(partner.mobile, partner_mobile)
# if SMS is uninstalled, phone_sanitized is not available on partner
if 'phone_sanitized' in partner:
self.assertEqual(partner.phone_sanitized, partner_mobile_sanitized,
self.assertEqual(partner.phone_sanitized, new_phone_sanitized,
'Partner sanitized should be computed on mobile')
@users('user_sales_manager')
@ -453,7 +513,6 @@ class TestCRMLead(TestCrmCommon):
'name': 'NoContact Partner',
'phone': '',
'email': '',
'mobile': '',
})
# This is a type == 'lead', not a type == 'opportunity'
@ -550,6 +609,41 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(lead.probability, 100.0)
self.assertEqual(lead.stage_id, self.stage_gen_won) # generic won stage has lower sequence than team won stage
def test_crm_lead_stages_with_multiple_possible_teams(self):
""" Test lead stage is properly set when switching between multiple teams. """
self.sales_team_2 = self.env['crm.team'].create({
'name': 'Test Sales Team 2',
'company_id': False,
'user_id': self.user_sales_manager.id,
})
self.sales_team_2_m1 = self.env['crm.team.member'].create({
'user_id': self.user_sales_leads.id,
'crm_team_id': self.sales_team_2.id,
})
user_teams = self.env['crm.team'].search([
('crm_team_member_all_ids.user_id', '=', self.user_sales_leads.id),
])
self.assertIn(self.sales_team_1, user_teams)
self.assertIn(self.sales_team_2, user_teams)
self.stage_team2_1 = self.env['crm.stage'].create({
'name': 'New (T2)',
'team_ids': [self.sales_team_2.id],
})
lead = self.env['crm.lead'].with_user(self.user_sales_leads).create({
'name': 'Test',
'contact_name': 'Test Contact',
'team_id': self.sales_team_1.id,
})
self.assertEqual(lead.team_id, self.sales_team_1)
self.assertEqual(lead.stage_id, self.stage_team1_1)
lead.team_id = self.sales_team_2
self.assertEqual(lead.team_id, self.sales_team_2)
self.assertEqual(lead.stage_id, self.stage_team2_1)
@users('user_sales_manager')
def test_crm_lead_unlink_calendar_event(self):
""" Test res_id / res_model is reset (and hide document button in calendar
@ -571,7 +665,7 @@ class TestCRMLead(TestCrmCommon):
'stop': '2022-07-13 10:00:00',
}
])
self.assertEqual(lead.calendar_event_count, 1)
self.assertEqual(len(lead.calendar_event_ids), 1)
self.assertEqual(meetings.opportunity_id, lead)
self.assertEqual(meetings.mapped('res_id'), [lead.id, lead.id])
self.assertEqual(meetings.mapped('res_model'), ['crm.lead', 'crm.lead'])
@ -728,28 +822,37 @@ class TestCRMLead(TestCrmCommon):
"""Test that the help message is the right one if we are on multiple team with different settings."""
# archive other teams
self.env['crm.team'].search([]).active = False
self.env['ir.config_parameter'].sudo().set_param("sales_team.membership_multi", True)
self._activate_multi_company()
team_other_comp = self.team_company2
user_team_leads, team_leads, user_team_opport, team_opport = self.env['crm.team'].create([{
'name': 'UserTeamLeads',
'company_id': self.env.company.id,
'use_leads': True,
'member_ids': [(6, 0, [self.env.user.id])],
}, {
'name': 'TeamLeads',
'company_id': self.env.company.id,
'use_leads': True,
'member_ids': [],
}, {
'name': 'UserTeamOpportunities',
'company_id': self.env.company.id,
'use_leads': False,
'member_ids': [(6, 0, [self.env.user.id])],
}, {
'name': 'TeamOpportunities',
'company_id': self.env.company.id,
'use_leads': False,
'member_ids': [],
}])
# Additional check to ensure proper team creation
user_team_leads.invalidate_recordset(fnames=['member_ids'])
self.assertEqual(user_team_leads.member_ids.ids, [self.env.user.id])
self.env['crm.lead'].create([{
'name': 'LeadOurTeam',
'team_id': user_team_leads.id,
@ -781,11 +884,10 @@ class TestCRMLead(TestCrmCommon):
for team in teams:
with self.subTest(team=team):
team_mail = f"{team.alias_name}@{team.alias_domain}"
if team != team_other_comp:
self.assertIn(f"<a href='mailto:{team_mail}'>{team_mail}</a>", self.env['crm.lead'].sudo().get_empty_list_help(""))
self.assertIn(f"<a href='mailto:{team.alias_email}'>{team.alias_email}</a>", self.env['crm.lead'].sudo().get_empty_list_help(""))
else:
self.assertNotIn(f"<a href='mailto:{team_mail}'>{team_mail}</a>", self.env['crm.lead'].sudo().get_empty_list_help(""))
self.assertNotIn(f"<a href='mailto:{team.alias_email}'>{team.alias_email}</a>", self.env['crm.lead'].sudo().get_empty_list_help(""))
team.active = False
@mute_logger('odoo.addons.mail.models.mail_thread')
@ -793,7 +895,7 @@ class TestCRMLead(TestCrmCommon):
new_lead = self.format_and_process(
INCOMING_EMAIL,
'unknown.sender@test.example.com',
'%s@%s' % (self.sales_team_1.alias_name, self.alias_domain),
self.sales_team_1.alias_email,
subject='Delivery cost inquiry',
target_model='crm.lead',
)
@ -802,49 +904,12 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(new_lead.name, 'Delivery cost inquiry')
message = new_lead.with_user(self.user_sales_manager).message_post(
body='Here is my offer !',
body='Here is my offer!',
subtype_xmlid='mail.mt_comment')
self.assertEqual(message.author_id, self.user_sales_manager.partner_id)
new_lead._handle_partner_assignment(create_missing=True)
self.assertEqual(new_lead.partner_id.email, 'unknown.sender@test.example.com')
self.assertEqual(new_lead.partner_id.team_id, self.sales_team_1)
@users('user_sales_manager')
def test_message_get_suggested_recipients(self):
"""This test checks that creating a contact from a lead with an inactive language will ignore the language
while creating a contact from a lead with an active language will take it into account """
ResLang = self.env['res.lang'].sudo().with_context(active_test=False)
# Create a lead with an inactive language -> should ignore the preset language
lang_fr = ResLang.search([('code', '=', 'fr_FR')])
if not lang_fr:
lang_fr = ResLang._create_lang('fr_FR')
# set French language as inactive then try to call "_message_get_suggested_recipients"
# -> lang code should be ignored
lang_fr.active = False
lead1 = self.env['crm.lead'].create({
'name': 'TestLead',
'email_from': self.test_email,
'lang_id': lang_fr.id,
})
data = lead1._message_get_suggested_recipients()[lead1.id]
self.assertEqual(data, [(False, self.test_email, None, 'Customer Email')])
# Create a lead with an active language -> should keep the preset language for recipients
lang_en = ResLang.search([('code', '=', 'en_US')])
if not lang_en:
lang_en = ResLang._create_lang('en_US')
# set American English language as active then try to call "_message_get_suggested_recipients"
# -> lang code should be kept
lang_en.active = True
lead2 = self.env['crm.lead'].create({
'name': 'TestLead',
'email_from': self.test_email,
'lang_id': lang_en.id,
})
data = lead2._message_get_suggested_recipients()[lead2.id]
self.assertEqual(data, [(False, self.test_email, "en_US", 'Customer Email')])
@users('user_sales_manager')
def test_phone_mobile_search(self):
@ -870,10 +935,9 @@ class TestCRMLead(TestCrmCommon):
})
# search term containing less than 3 characters should throw an error (some currently not working)
self.env['crm.lead'].search([('phone_mobile_search', 'like', '')]) # no restriction, returns all
with self.assertRaises(UserError):
self.env['crm.lead'].search([('phone_mobile_search', 'like', '')])
# with self.assertRaises(UserError):
# self.env['crm.lead'].search([('phone_mobile_search', 'like', '7 ')])
self.env['crm.lead'].search([('phone_mobile_search', 'like', '7 ')])
with self.assertRaises(UserError):
self.env['crm.lead'].search([('phone_mobile_search', 'like', 'c')])
with self.assertRaises(UserError):
@ -960,27 +1024,193 @@ class TestCRMLead(TestCrmCommon):
'phone': self.test_phone_data[0],
})
self.assertEqual(lead.phone, self.test_phone_data[0])
self.assertFalse(lead.mobile)
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[0])
lead.write({'phone': False, 'mobile': self.test_phone_data[1]})
lead.write({'phone': False})
self.assertFalse(lead.phone)
self.assertEqual(lead.mobile, self.test_phone_data[1])
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[1])
self.assertEqual(lead.phone_sanitized, False)
lead.write({'phone': self.test_phone_data[1], 'mobile': self.test_phone_data[2]})
lead.write({'phone': self.test_phone_data[1]})
self.assertEqual(lead.phone, self.test_phone_data[1])
self.assertEqual(lead.mobile, self.test_phone_data[2])
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[2])
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[1])
# updating country should trigger sanitize computation
lead.write({'country_id': self.env.ref('base.be').id})
self.assertEqual(lead.phone, self.test_phone_data[1])
self.assertEqual(lead.mobile, self.test_phone_data[2])
self.assertFalse(lead.phone_sanitized)
class TestLeadFormatAddress(FormatAddressCase):
class TestCRMLeadRotting(TestCrmCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.stage_team1_1.rotting_threshold_days = 5
cls.stage_team1_2.rotting_threshold_days = 3
@users('user_sales_manager')
def test_leads_rotting(self):
rotten_leads = self.env['crm.lead']
clean_leads = self.env['crm.lead']
close_future = datetime(2025, 1, 24, 12, 0, 0)
now = datetime(2025, 1, 20, 12, 0, 0)
close_past = datetime(2025, 1, 18, 12, 0, 0)
past = datetime(2025, 1, 10, 12, 0, 0)
last_year = datetime(2024, 1, 20, 12, 0, 0)
with self.mock_datetime_and_now(past):
rotten_leads += self.env['crm.lead'].create([
{
'name': 'Opportunity',
'type': 'opportunity',
'stage_id': self.stage_team1_1.id,
} for x in range(3)
])
rotten_leads.flush_recordset(['date_last_stage_update']) # precalculate stage update
with self.mock_datetime_and_now(close_past):
clean_leads += self.env['crm.lead'].create({
'name': "Lead that won't have time to rot",
'type': 'opportunity',
'stage_id': self.stage_team1_1.id,
})
clean_leads.flush_recordset(['date_last_stage_update']) # precalculate stage update
with self.mock_datetime_and_now(last_year):
clean_leads += self.env['crm.lead'].create({
'name': 'Opportuniy in Won Stage',
'type': 'opportunity',
'stage_id': self.stage_gen_won.id,
})
clean_leads.flush_recordset(['date_last_stage_update']) # precalculate stage update
with self.mock_datetime_and_now(now):
for lead in rotten_leads:
self.assertTrue(lead.is_rotting)
self.assertEqual(lead.rotting_days, 10)
for lead in clean_leads:
self.assertFalse(lead.is_rotting)
self.assertEqual(lead.rotting_days, 0)
rotten_leads_iterator = iter(rotten_leads)
lead_edited = next(rotten_leads_iterator)
lead_edited.name = 'Edited Opportunity'
self.assertTrue(
lead_edited.is_rotting,
'Editing the lead has no effect on rotting status',
)
lead_changed_stage = next(rotten_leads_iterator)
lead_changed_stage.stage_id = self.stage_team1_2.id
self.assertFalse(
lead_changed_stage.is_rotting,
'Changing the stage disables rotting status',
)
lead_changed_rotting_threshold = next(rotten_leads_iterator)
old_rotting_threshold = self.stage_team1_1.rotting_threshold_days
self.stage_team1_1.rotting_threshold_days = 50
self.assertFalse(
lead_changed_rotting_threshold.is_rotting,
'Changing the rotting threshold to a higher value does affect rotten leads\' status',
)
self.stage_team1_1.rotting_threshold_days = old_rotting_threshold # Revert rotting threshold
self.assertTrue(
lead_changed_rotting_threshold.is_rotting,
'Changing the threshold back should affect the status again',
)
self.stage_team1_1.rotting_threshold_days = 0
self.assertFalse(
lead_changed_rotting_threshold.is_rotting,
'A 0-day rotting threshold disables rotting',
)
self.stage_team1_1.rotting_threshold_days = old_rotting_threshold
# create a new lead in the New stage
jan20_lead = self.env['crm.lead'].create({
'name': 'Fresh Opportuniy',
'type': 'opportunity',
'stage_id': self.stage_team1_1.id,
})
# 4 days later:
with self.mock_datetime_and_now(close_future):
rotten_leads.invalidate_recordset(['is_rotting', 'rotting_days'])
self.assertEqual(
lead_changed_rotting_threshold.rotting_days,
14,
'Since this lead has not seen a stage change, it has been rotting for 14 days total',
)
self.assertFalse(
jan20_lead.is_rotting,
'Since this lead remained in a stage with a higher threshold, it\'s not rotting yet',
)
self.assertTrue(
lead_changed_stage.is_rotting,
'As its new stage has a lower rotting threshold, this lead should be rotting 3 days after its last stage change',
)
self.assertEqual(lead_changed_stage.rotting_days, 4)
def test_search_leads_rotting(self):
"""
This test checks that the result of search_leads_rotting accurately matches is_rotting computation results
"""
past = datetime(2025, 1, 1)
now = datetime(2025, 1, 10)
with self.mock_datetime_and_now(past):
all_leads = self.env['crm.lead'].create([{
'name': 'TestLead Rotting opportunity',
'type': 'opportunity',
'stage_id': self.stage_team1_1.id,
}] * 5 + [{
'name': 'TestLead Lead',
'type': 'lead',
'stage_id': self.stage_team1_1.id,
}] * 3 + [{
'name': 'TestLead Won Opportunity',
'type': 'opportunity',
'stage_id': self.stage_gen_won.id,
}] * 4)
all_leads.flush_recordset(['date_last_stage_update'])
rotten_leads = all_leads.filtered(lambda lead: 'Rotting' in lead.name)
clean_leads = all_leads - rotten_leads
with self.mock_datetime_and_now(now):
rot = self.env['crm.lead'].search([
('name', 'ilike', 'TestLead'),
('is_rotting', '=', True),
], order='id ASC')
norot = self.env['crm.lead'].search([
('name', 'ilike', 'TestLead'),
('is_rotting', '=', False),
], order='id ASC')
self.assertEqual(rot, rotten_leads)
self.assertEqual(norot, clean_leads)
@tagged('lead_internals')
class TestLeadFormTools(FormatAddressCase):
def test_address_view(self):
self.env.company.country_id = self.env.ref('base.us')
self.assertAddressView('crm.lead')
@tagged('lead_internals', 'is_query_count')
class TestCrmLeadMailTrackingDuration(MailTrackingDurationMixinCase):
@classmethod
def setUpClass(cls):
super().setUpClass('crm.lead')
def test_crm_lead_mail_tracking_duration(self):
self._test_record_duration_tracking()
def test_crm_lead_mail_tracking_duration_batch(self):
self._test_record_duration_tracking_batch()
def test_crm_lead_queries_batch_mail_tracking_duration(self):
self._test_queries_batch_duration_tracking()