Initial commit: Crm packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:49 +02:00
commit 21a345b5b9
654 changed files with 418312 additions and 0 deletions

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_crm_activity
from . import test_crm_lead
from . import test_crm_lead_assignment
from . import test_crm_lead_notification
from . import test_crm_lead_convert
from . import test_crm_lead_convert_mass
from . import test_crm_lead_duplicates
from . import test_crm_lead_lost
from . import test_crm_lead_merge
from . import test_crm_lead_multicompany
from . import test_crm_lead_smart_calendar
from . import test_crm_ui
from . import test_crm_pls
from . import test_performances
from . import test_res_partner
from . import test_sales_team_ui

View file

@ -0,0 +1,727 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from contextlib import contextmanager
from unittest.mock import patch
from odoo.addons.crm.models.crm_lead import PARTNER_ADDRESS_FIELDS_TO_SYNC
from odoo.addons.mail.tests.common import MailCase, mail_new_test_user
from odoo.addons.phone_validation.tools import phone_validation
from odoo.addons.sales_team.tests.common import TestSalesCommon
from odoo.fields import Datetime
from odoo import models, tools
INCOMING_EMAIL = """Return-Path: {return_path}
X-Original-To: {to}
Delivered-To: {to}
Received: by mail.my.com (Postfix, from userid xxx)
id 822ECBFB67; Mon, 24 Oct 2011 07:36:51 +0200 (CEST)
X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on mail.my.com
X-Spam-Level:
X-Spam-Status: No, score=-1.0 required=5.0 tests=ALL_TRUSTED autolearn=ham
version=3.3.1
Received: from [192.168.1.146]
(Authenticated sender: {email_from})
by mail.customer.com (Postfix) with ESMTPSA id 07A30BFAB4
for <{to}>; Mon, 24 Oct 2011 07:36:50 +0200 (CEST)
Message-ID: {msg_id}
Date: Mon, 24 Oct 2011 11:06:29 +0530
From: {email_from}
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.14) Gecko/20110223 Lightning/1.0b2 Thunderbird/3.1.8
MIME-Version: 1.0
To: {to}
Subject: {subject}
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 8bit
This is an example email. All sensitive content has been stripped out.
ALL GLORY TO THE HYPNOTOAD !
Cheers,
Somebody."""
class TestCrmCommon(TestSalesCommon, MailCase):
FIELDS_FIRST_SET = [
'name', 'partner_id', 'campaign_id', 'company_id', 'country_id',
'team_id', 'state_id', 'stage_id', 'medium_id', 'source_id', 'user_id',
'title', 'city', 'contact_name', 'mobile', 'partner_name',
'phone', 'probability', 'expected_revenue', 'street', 'street2', 'zip',
'create_date', 'date_action_last', 'email_from', 'email_cc', 'website'
]
merge_fields = ['description', 'type', 'priority']
@classmethod
def setUpClass(cls):
super(TestCrmCommon, cls).setUpClass()
cls._init_mail_gateway()
# Salesmen organization
# ------------------------------------------------------------
# Role: M (team member) R (team manager)
# SALESMAN---------------sales_team_1
# admin------------------M-----------
# user_sales_manager-----R-----------
# user_sales_leads-------M-----------
# user_sales_salesman----/-----------
# Sales teams organization
# ------------------------------------------------------------
# SALESTEAM-----------SEQU-----COMPANY
# sales_team_1--------5--------False
# data----------------9999-----??
cls.sales_team_1.write({
'alias_name': 'sales.test',
'use_leads': True,
'use_opportunities': True,
'assignment_domain': False,
})
cls.sales_team_1_m1.write({
'assignment_max': 45,
'assignment_domain': False,
})
cls.sales_team_1_m2.write({
'assignment_max': 15,
'assignment_domain': False,
})
(cls.user_sales_manager + cls.user_sales_leads + cls.user_sales_salesman).write({
'groups_id': [(4, cls.env.ref('crm.group_use_lead').id)]
})
cls.env['crm.stage'].search([]).write({'sequence': 9999}) # ensure search will find test data first
cls.stage_team1_1 = cls.env['crm.stage'].create({
'name': 'New',
'sequence': 1,
'team_id': cls.sales_team_1.id,
})
cls.stage_team1_2 = cls.env['crm.stage'].create({
'name': 'Proposition',
'sequence': 5,
'team_id': cls.sales_team_1.id,
})
cls.stage_team1_won = cls.env['crm.stage'].create({
'name': 'Won',
'sequence': 70,
'team_id': cls.sales_team_1.id,
'is_won': True,
})
cls.stage_gen_1 = cls.env['crm.stage'].create({
'name': 'Generic stage',
'sequence': 3,
'team_id': False,
})
cls.stage_gen_won = cls.env['crm.stage'].create({
'name': 'Generic Won',
'sequence': 30,
'team_id': False,
'is_won': True,
})
# countries and langs
base_us = cls.env.ref('base.us')
cls.env['res.lang']._activate_lang('fr_FR')
cls.env['res.lang']._activate_lang('en_US')
cls.lang_en = cls.env['res.lang']._lang_get('en_US')
cls.lang_fr = cls.env['res.lang']._lang_get('fr_FR')
# leads
cls.lead_1 = cls.env['crm.lead'].create({
'name': 'Nibbler Spacecraft Request',
'type': 'lead',
'user_id': cls.user_sales_leads.id,
'team_id': cls.sales_team_1.id,
'partner_id': False,
'contact_name': 'Amy Wong',
'email_from': 'amy.wong@test.example.com',
'lang_id': cls.lang_fr.id,
'phone': '+1 202 555 9999',
'country_id': cls.env.ref('base.us').id,
'probability': 20,
})
# update lead_1: stage_id is not computed anymore by default for leads
cls.lead_1.write({
'stage_id': cls.stage_team1_1.id,
})
# create an history for new team
cls.lead_team_1_won = cls.env['crm.lead'].create({
'name': 'Already Won',
'type': 'lead',
'user_id': cls.user_sales_leads.id,
'team_id': cls.sales_team_1.id,
})
cls.lead_team_1_won.action_set_won()
cls.lead_team_1_lost = cls.env['crm.lead'].create({
'name': 'Already Won',
'type': 'lead',
'user_id': cls.user_sales_leads.id,
'team_id': cls.sales_team_1.id,
})
cls.lead_team_1_lost.action_set_lost()
(cls.lead_team_1_won + cls.lead_team_1_lost).flush_recordset()
# email / phone data
cls.test_email_data = [
'"Planet Express" <planet.express@test.example.com>',
'"Philip, J. Fry" <philip.j.fry@test.example.com>',
'"Turanga Leela" <turanga.leela@test.example.com>',
]
cls.test_email_data_normalized = [
'planet.express@test.example.com',
'philip.j.fry@test.example.com',
'turanga.leela@test.example.com',
]
cls.test_phone_data = [
'+1 202 555 0122', # formatted US number
'202 555 0999', # local US number
'202 555 0888', # local US number
]
cls.test_phone_data_sanitized = [
'+12025550122',
'+12025550999',
'+12025550888',
]
# create some test contact and companies
cls.contact_company_1 = cls.env['res.partner'].create({
'name': 'Planet Express',
'email': cls.test_email_data[0],
'is_company': True,
'street': '57th Street',
'city': 'New New York',
'country_id': cls.env.ref('base.us').id,
'zip': '12345',
})
cls.contact_1 = cls.env['res.partner'].create({
'name': 'Philip J Fry',
'email': cls.test_email_data[1],
'mobile': cls.test_phone_data[0],
'title': cls.env.ref('base.res_partner_title_mister').id,
'function': 'Delivery Boy',
'lang': cls.lang_en.code,
'phone': False,
'parent_id': cls.contact_company_1.id,
'is_company': False,
'street': 'Actually the sewers',
'city': 'New York',
'country_id': cls.env.ref('base.us').id,
'zip': '54321',
})
cls.contact_2 = cls.env['res.partner'].create({
'name': 'Turanga Leela',
'email': cls.test_email_data[2],
'lang': cls.lang_en.code,
'mobile': cls.test_phone_data[1],
'phone': cls.test_phone_data[2],
'parent_id': False,
'is_company': False,
'street': 'Cookieville Minimum-Security Orphanarium',
'city': 'New New York',
'country_id': cls.env.ref('base.us').id,
'zip': '97648',
})
cls.contact_company = cls.env['res.partner'].create({
'name': 'Mom',
'company_name': 'MomCorp',
'is_company': True,
'street': 'Mom Friendly Robot Street',
'city': 'New new York',
'country_id': base_us.id,
'lang': cls.lang_en.code,
'mobile': '+1 202 555 0888',
'zip': '87654',
})
# test activities
cls.activity_type_1 = cls.env['mail.activity.type'].create({
'name': 'Lead Test Activity 1',
'summary': 'ACT 1 : Presentation, barbecue, ... ',
'res_model': 'crm.lead',
'category': 'meeting',
'delay_count': 5,
})
cls.env['ir.model.data'].create({
'name': cls.activity_type_1.name.lower().replace(' ', '_'),
'module': 'crm',
'model': cls.activity_type_1._name,
'res_id': cls.activity_type_1.id,
})
def setUp(self):
super(TestCrmCommon, self).setUp()
self.flush_tracking()
@classmethod
def _activate_multi_company(cls):
cls.company_2 = cls.env['res.company'].create({
'country_id': cls.env.ref('base.au').id,
'currency_id': cls.env.ref('base.AUD').id,
'email': 'company.2@test.example.com',
'name': 'New Test Company',
})
cls.user_sales_manager_mc = mail_new_test_user(
cls.env,
company_id=cls.company_2.id,
company_ids=[(4, cls.company_main.id), (4, cls.company_2.id)],
email='user.sales.manager.mc@test.example.com',
login='user_sales_manager_mc',
groups='sales_team.group_sale_manager,base.group_partner_manager',
name='Myrddin Sales Manager',
notification_type='inbox',
)
cls.team_company2 = cls.env['crm.team'].create({
'company_id': cls.company_2.id,
'name': 'C2 Team',
'sequence': 10,
'user_id': False,
})
cls.team_company2_m1 = cls.env['crm.team.member'].create({
'crm_team_id': cls.team_company2.id,
'user_id': cls.user_sales_manager_mc.id,
'assignment_max': 30,
'assignment_domain': False,
})
cls.team_company1 = cls.env['crm.team'].create({
'company_id': cls.company_main.id,
'name': 'MainCompany Team',
'sequence': 50,
'user_id': cls.user_sales_manager.id,
})
cls.partner_c2 = cls.env['res.partner'].create({
'company_id': cls.company_2.id,
'email': '"Partner C2" <partner_c2@multicompany.example.com>',
'name': 'Customer for C2',
'phone': '+32455001122',
})
def _create_leads_batch(self, lead_type='lead', count=10, email_dup_count=0,
partner_count=0, partner_ids=None, user_ids=None,
country_ids=None, probabilities=None, suffix=''):
""" Helper tool method creating a batch of leads, useful when dealing
with batch processes. Please update me.
:param string type: 'lead', 'opportunity', 'mixed' (lead then opp),
None (depends on configuration);
:param partner_count: if not partner_ids is given, generate partner count
customers; other leads will have no customer;
:param partner_ids: a set of partner ids to cycle when creating leads;
:param user_ids: a set of user ids to cycle when creating leads;
:return: create leads
"""
types = ['lead', 'opportunity']
leads_data = [{
'name': f'TestLead{suffix}_{x:04d}',
'type': lead_type if lead_type else types[x % 2],
'priority': '%s' % (x % 3),
} for x in range(count)]
# generate customer information
partners = []
if partner_count:
partners = self.env['res.partner'].create([{
'name': 'AutoPartner_%04d' % (x),
'email': tools.formataddr((
'AutoPartner_%04d' % (x),
'partner_email_%04d@example.com' % (x),
)),
} for x in range(partner_count)])
# customer information
if partner_ids:
for idx, lead_data in enumerate(leads_data):
lead_data['partner_id'] = partner_ids[idx % len(partner_ids)]
else:
for idx, lead_data in enumerate(leads_data):
if partner_count and idx < partner_count:
lead_data['partner_id'] = partners[idx].id
else:
lead_data['email_from'] = tools.formataddr((
'TestCustomer_%02d' % (idx),
'customer_email_%04d@example.com' % (idx)
))
# country + phone information
if country_ids:
cid_to_country = dict(
(country.id, country)
for country in self.env['res.country'].browse([cid for cid in country_ids if cid])
)
for idx, lead_data in enumerate(leads_data):
country_id = country_ids[idx % len(country_ids)]
country = cid_to_country.get(country_id, self.env['res.country'])
lead_data['country_id'] = country.id
if lead_data['country_id']:
lead_data['phone'] = phone_validation.phone_format(
'0456%04d99' % (idx),
country.code, country.phone_code,
force_format='E164')
else:
lead_data['phone'] = '+32456%04d99' % (idx)
# salesteam information
if user_ids:
for idx, lead_data in enumerate(leads_data):
lead_data['user_id'] = user_ids[idx % len(user_ids)]
# probabilities
if probabilities:
for idx, lead_data in enumerate(leads_data):
lead_data['probability'] = probabilities[idx % len(probabilities)]
# duplicates (currently only with email)
dups_data = []
if email_dup_count and not partner_ids:
for idx, lead_data in enumerate(leads_data):
if not lead_data.get('partner_id') and lead_data['email_from']:
dup_data = dict(lead_data)
dup_data['name'] = 'Duplicated-%s' % dup_data['name']
dups_data.append(dup_data)
if len(dups_data) >= email_dup_count:
break
return self.env['crm.lead'].create(leads_data + dups_data)
def _create_duplicates(self, lead, create_opp=True):
""" Helper tool method creating, based on a given lead
* a customer (res.partner) based on lead email (to test partner finding)
-> FIXME: using same normalized email does not work currently, only exact email works
* a lead with same email_from
* a lead with same email_normalized (other email_from)
* a lead with customer but another email
* a lost opportunity with same email_from
"""
customer = self.env['res.partner'].create({
'name': 'Lead1 Email Customer',
'email': lead.email_from,
})
lead_email_from = self.env['crm.lead'].create({
'name': 'Duplicate: same email_from',
'type': 'lead',
'team_id': lead.team_id.id,
'email_from': lead.email_from,
'probability': lead.probability,
})
lead_email_normalized = self.env['crm.lead'].create({
'name': 'Duplicate: email_normalize comparison',
'type': 'lead',
'team_id': lead.team_id.id,
'stage_id': lead.stage_id.id,
'email_from': 'CUSTOMER WITH NAME <%s>' % lead.email_normalized.upper(),
'probability': lead.probability,
})
lead_partner = self.env['crm.lead'].create({
'name': 'Duplicate: customer ID',
'type': 'lead',
'team_id': lead.team_id.id,
'partner_id': customer.id,
'probability': lead.probability,
})
if create_opp:
opp_lost = self.env['crm.lead'].create({
'name': 'Duplicate: lost opportunity',
'type': 'opportunity',
'team_id': lead.team_id.id,
'stage_id': lead.stage_id.id,
'email_from': lead.email_from,
'probability': lead.probability,
})
opp_lost.action_set_lost()
else:
opp_lost = self.env['crm.lead']
new_leads = lead_email_from + lead_email_normalized + lead_partner + opp_lost
new_leads.flush_recordset() # compute notably probability
return customer, new_leads
@contextmanager
def assertLeadMerged(self, opportunity, leads, **expected):
""" Assert result of lead _merge_opportunity process. This is done using
a context manager in order to save original opportunity (master lead)
values. Indeed those will be modified during merge process. We have to
ensure final values are correct taking into account all leads values
before merging them.
:param opportunity: final opportunity
:param leads: merged leads (including opportunity)
"""
self.assertIn(opportunity, leads)
# save opportunity value before being modified by merge process
fields_all = self.FIELDS_FIRST_SET + self.merge_fields
original_opp_values = dict(
(fname, opportunity[fname])
for fname in fields_all
if fname in opportunity
)
def _find_value(lead, fname):
if lead == opportunity:
return original_opp_values[fname]
return lead[fname]
def _first_set(fname):
values = [_find_value(lead, fname) for lead in leads]
return next((value for value in values if value), False)
def _get_type():
values = [_find_value(lead, 'type') for lead in leads]
return 'opportunity' if 'opportunity' in values else 'lead'
def _get_description():
values = [_find_value(lead, 'description') for lead in leads]
return '<br><br>'.join(value for value in values if value)
def _get_priority():
values = [_find_value(lead, 'priority') for lead in leads]
return max(values)
def _aggregate(fname):
if isinstance(self.env['crm.lead'][fname], models.BaseModel):
values = leads.mapped(fname)
else:
values = [_find_value(lead, fname) for lead in leads]
return values
try:
# merge process will modify opportunity
yield
finally:
# support specific values caller may want to check in addition to generic tests
for fname, expected in expected.items():
if expected is False:
self.assertFalse(opportunity[fname], "%s must be False" % fname)
else:
self.assertEqual(opportunity[fname], expected, "%s must be equal to %s" % (fname, expected))
# classic fields: first not void wins or specific computation
for fname in fields_all:
if fname not in opportunity: # not all fields available when doing -u
continue
opp_value = opportunity[fname]
if fname == 'description':
self.assertEqual(opp_value, _get_description())
elif fname == 'type':
self.assertEqual(opp_value, _get_type())
elif fname == 'priority':
self.assertEqual(opp_value, _get_priority())
elif fname in ('order_ids', 'visitor_ids'):
self.assertEqual(opp_value, _aggregate(fname))
elif fname in PARTNER_ADDRESS_FIELDS_TO_SYNC:
# Specific computation, has its own test
continue
else:
self.assertEqual(
opp_value if opp_value or not isinstance(opp_value, models.BaseModel) else False,
_first_set(fname)
)
class TestLeadConvertCommon(TestCrmCommon):
@classmethod
def setUpClass(cls):
super(TestLeadConvertCommon, cls).setUpClass()
# Sales Team organization
# Role: M (team member) R (team manager)
# SALESMAN---------------sales_team_1-----sales_team_convert
# admin------------------M----------------/ (sales_team_1_m2)
# user_sales_manager-----R----------------R
# user_sales_leads-------M----------------/ (sales_team_1_m1)
# user_sales_salesman----/----------------M (sales_team_convert_m1)
# Stages Team organization
# Name-------------------ST-------------------Sequ
# stage_team1_1----------sales_team_1---------1
# stage_team1_2----------sales_team_1---------5
# stage_team1_won--------sales_team_1---------70
# stage_gen_1------------/--------------------3
# stage_gen_won----------/--------------------30
# stage_team_convert_1---sales_team_convert---1
cls.sales_team_convert = cls.env['crm.team'].create({
'name': 'Convert Sales Team',
'sequence': 10,
'alias_name': False,
'use_leads': True,
'use_opportunities': True,
'company_id': False,
'user_id': cls.user_sales_manager.id,
'assignment_domain': [('priority', 'in', ['1', '2', '3'])],
})
cls.sales_team_convert_m1 = cls.env['crm.team.member'].create({
'user_id': cls.user_sales_salesman.id,
'crm_team_id': cls.sales_team_convert.id,
'assignment_max': 30,
'assignment_domain': False,
})
cls.stage_team_convert_1 = cls.env['crm.stage'].create({
'name': 'New',
'sequence': 1,
'team_id': cls.sales_team_convert.id,
})
cls.lead_1.write({'date_open': Datetime.from_string('2020-01-15 11:30:00')})
cls.crm_lead_dt_patcher = patch('odoo.addons.crm.models.crm_lead.fields.Datetime', wraps=Datetime)
cls.crm_lead_dt_mock = cls.startClassPatcher(cls.crm_lead_dt_patcher)
@classmethod
def _switch_to_multi_membership(cls):
# Sales Team organization
# Role: M (team member) R (team manager)
# SALESMAN---------------sales_team_1-----sales_team_convert
# admin------------------M----------------/ (sales_team_1_m2)
# user_sales_manager-----R----------------R+M <-- NEW (sales_team_convert_m2)
# user_sales_leads-------M----------------/ (sales_team_1_m1)
# user_sales_salesman----M----------------M <-- NEW (sales_team_1_m3 / sales_team_convert_m1)
# SALESMAN--------------sales_team----------assign_max
# admin-----------------sales_team_1--------15 (tot: 0.5/day)
# user_sales_manager----sales_team_convert--60 (tot: 2/day)
# user_sales_leads------sales_team_1--------45 (tot: 1.5/day)
# user_sales_salesman---sales_team_1--------15 (tot: 1.5/day)
# user_sales_salesman---sales_team_convert--30
cls.sales_team_1_m1.write({
'assignment_max': 45,
'assignment_domain': False,
})
cls.sales_team_1_m2.write({
'assignment_max': 15,
'assignment_domain': [('probability', '>=', 10)],
})
cls.env['ir.config_parameter'].set_param('sales_team.membership_multi', True)
cls.sales_team_1_m3 = cls.env['crm.team.member'].create({
'user_id': cls.user_sales_salesman.id,
'crm_team_id': cls.sales_team_1.id,
'assignment_max': 15,
'assignment_domain': [('probability', '>=', 20)],
})
cls.sales_team_convert_m1.write({
'assignment_max': 30,
'assignment_domain': [('probability', '>=', 20)]
})
cls.sales_team_convert_m2 = cls.env['crm.team.member'].create({
'user_id': cls.user_sales_manager.id,
'crm_team_id': cls.sales_team_convert.id,
'assignment_max': 60,
'assignment_domain': False,
})
@classmethod
def _switch_to_auto_assign(cls):
cls.env['ir.config_parameter'].set_param('crm.lead.auto.assignment', True)
cls.assign_cron = cls.env.ref('crm.ir_cron_crm_lead_assign')
cls.assign_cron.update({
'active': True,
'interval_type': 'days',
'interval_number': 1,
})
def assertMemberAssign(self, member, count):
""" Check assign result and that domains are effectively taken into account """
self.assertEqual(member.lead_month_count, count)
member_leads = self.env['crm.lead'].search(member._get_lead_month_domain())
self.assertEqual(len(member_leads), count)
if member.assignment_domain:
self.assertEqual(
member_leads.filtered_domain(literal_eval(member.assignment_domain)),
member_leads
)
# TODO this condition is not fulfilled in case of merge, need to change merge/assignment process
# if member.crm_team_id.assignment_domain:
# self.assertEqual(
# member_leads.filtered_domain(literal_eval(member.crm_team_id.assignment_domain)),
# member_leads,
# 'Assign domain not matching: %s' % member.crm_team_id.assignment_domain
# )
class TestLeadConvertMassCommon(TestLeadConvertCommon):
@classmethod
def setUpClass(cls):
super(TestLeadConvertMassCommon, cls).setUpClass()
# Sales Team organization
# Role: M (team member) R (team manager)
# SALESMAN-------------------sales_team_1-----sales_team_convert
# admin----------------------M----------------/ (sales_team_1_m2)
# user_sales_manager---------R----------------R (sales_team_1_m1)
# user_sales_leads-----------M----------------/
# user_sales_leads_convert---/----------------M <-- NEW (sales_team_convert_m2)
# user_sales_salesman--------/----------------M (sales_team_convert_m1)
cls.user_sales_leads_convert = mail_new_test_user(
cls.env, login='user_sales_leads_convert',
name='Lucien Sales Leads Convert', email='crm_leads_2@test.example.com',
company_id=cls.env.ref("base.main_company").id,
notification_type='inbox',
groups='sales_team.group_sale_salesman_all_leads,base.group_partner_manager,crm.group_use_lead',
)
cls.sales_team_convert_m2 = cls.env['crm.team.member'].create({
'user_id': cls.user_sales_leads_convert.id,
'crm_team_id': cls.sales_team_convert.id,
})
cls.lead_w_partner = cls.env['crm.lead'].create({
'name': 'New1',
'type': 'lead',
'priority': '0',
'probability': 10,
'user_id': cls.user_sales_manager.id,
'stage_id': False,
'partner_id': cls.contact_1.id,
})
cls.lead_w_partner.write({'stage_id': False})
cls.tags = cls.env['crm.tag'].create([{'name': 'Tag %i' % i} for i in range(4)])
cls.lead_1.tag_ids = cls.tags[:3]
cls.lead_w_partner_company = cls.env['crm.lead'].create({
'name': 'New1',
'type': 'lead',
'probability': 50,
'user_id': cls.user_sales_manager.id,
'stage_id': cls.stage_team1_1.id,
'partner_id': cls.contact_company_1.id,
'contact_name': 'Hermes Conrad',
'email_from': 'hermes.conrad@test.example.com',
'tag_ids': (cls.tags[:2] | cls.tags[3]),
})
cls.lead_w_contact = cls.env['crm.lead'].create({
'name': 'LeadContact',
'type': 'lead',
'probability': 25,
'contact_name': 'TestContact',
'user_id': cls.user_sales_salesman.id,
'stage_id': cls.stage_gen_1.id,
})
cls.lead_w_email = cls.env['crm.lead'].create({
'name': 'LeadEmailAsContact',
'type': 'lead',
'priority': '2',
'probability': 15,
'email_from': 'contact.email@test.example.com',
'user_id': cls.user_sales_salesman.id,
'stage_id': cls.stage_gen_1.id,
})
cls.lead_w_email_lost = cls.env['crm.lead'].create({
'name': 'Lost',
'type': 'lead',
'probability': 15,
'email_from': 'strange.from@test.example.com',
'user_id': cls.user_sales_leads.id,
'stage_id': cls.stage_team1_2.id,
'active': False,
})
cls.env.flush_all()

View file

@ -0,0 +1,177 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, timedelta
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.tests.common import users
class TestCrmMailActivity(TestCrmCommon):
@classmethod
def setUpClass(cls):
super(TestCrmMailActivity, cls).setUpClass()
cls.activity_type_1 = cls.env['mail.activity.type'].create({
'name': 'Initial Contact',
'delay_count': 5,
'summary': 'ACT 1 : Presentation, barbecue, ... ',
'res_model': 'crm.lead',
})
cls.activity_type_2 = cls.env['mail.activity.type'].create({
'name': 'Call for Demo',
'delay_count': 6,
'summary': 'ACT 2 : I want to show you my ERP !',
'res_model': 'crm.lead',
})
for activity_type in cls.activity_type_1 + cls.activity_type_2:
cls.env['ir.model.data'].create({
'name': activity_type.name.lower().replace(' ', '_'),
'module': 'crm',
'model': activity_type._name,
'res_id': activity_type.id,
})
@users('user_sales_leads')
def test_crm_activity_ordering(self):
""" Test ordering on "my activities", linked to a hack introduced for a b2b.
Purpose is to be able to order on "my activities", which is a filtered o2m.
In this test we will check search, limit, order and offset linked to the
override of search for this non stored computed field. """
# Initialize some batch data
default_order = self.env['crm.lead']._order
self.assertEqual(default_order, "priority desc, id desc") # force updating this test is order changes
test_leads = self._create_leads_batch(count=10, partner_ids=[self.contact_1.id, self.contact_2.id, False]).sorted('id')
# assert initial data, ensure we did not break base behavior
for lead in test_leads:
self.assertFalse(lead.my_activity_date_deadline)
search_res = self.env['crm.lead'].search([('id', 'in', test_leads.ids)], limit=5, offset=0, order='id ASC')
self.assertEqual(search_res.ids, test_leads[:5].ids)
search_res = self.env['crm.lead'].search([('id', 'in', test_leads.ids)], limit=5, offset=5, order='id ASC')
self.assertEqual(search_res.ids, test_leads[5:10].ids)
# Let's schedule some activities for "myself" and "my bro"and check those are correctly computed
# LEAD NUMBER DEADLINE (MY) PRIORITY LATE DEADLINE (MY)
# 0 +2D 0 +2D
# 1 -1D 1 +2D
# 2 -2D 2 +2D
# 3 -1D 0 -1D
# 4 -2D 1 -2D
# 5 +2D 2 +2D
# 6+ / 0/1/2/0 /
today = date.today()
deadline_in2d, deadline_in1d = today + timedelta(days=2), today + timedelta(days=1)
deadline_was2d, deadline_was1d = today + timedelta(days=-2), today + timedelta(days=-1)
deadlines_my = [deadline_in2d, deadline_was1d, deadline_was2d, deadline_was1d, deadline_was2d,
deadline_in2d, False, False, False, False]
deadlines_gl = [deadline_in1d, deadline_was1d, deadline_was2d, deadline_was1d, deadline_was2d,
deadline_in2d, False, False, False, False]
test_leads[0:4].activity_schedule(act_type_xmlid='crm.call_for_demo', user_id=self.user_sales_manager.id, date_deadline=deadline_in1d)
test_leads[0:3].activity_schedule(act_type_xmlid='crm.initial_contact', date_deadline=deadline_in2d)
test_leads[5].activity_schedule(act_type_xmlid='crm.initial_contact', date_deadline=deadline_in2d)
(test_leads[1] | test_leads[3]).activity_schedule(act_type_xmlid='crm.initial_contact', date_deadline=deadline_was1d)
(test_leads[2] | test_leads[4]).activity_schedule(act_type_xmlid='crm.call_for_demo', date_deadline=deadline_was2d)
test_leads.invalidate_recordset()
expected_ids_asc = [2, 4, 1, 3, 5, 0, 8, 7, 9, 6]
expected_leads_asc = self.env['crm.lead'].browse([test_leads[lid].id for lid in expected_ids_asc])
expected_ids_desc = [5, 0, 1, 3, 2, 4, 8, 7, 9, 6]
expected_leads_desc = self.env['crm.lead'].browse([test_leads[lid].id for lid in expected_ids_desc])
for idx, lead in enumerate(test_leads):
self.assertEqual(lead.my_activity_date_deadline, deadlines_my[idx])
self.assertEqual(lead.activity_date_deadline, deadlines_gl[idx], 'Fail at %s' % idx)
# Let's go for a first batch of search
_order = 'my_activity_date_deadline ASC, %s' % default_order
_domain = [('id', 'in', test_leads.ids)]
search_res = self.env['crm.lead'].search(_domain, limit=None, offset=0, order=_order)
self.assertEqual(expected_leads_asc.ids, search_res.ids)
search_res = self.env['crm.lead'].search(_domain, limit=4, offset=0, order=_order)
self.assertEqual(expected_leads_asc[:4].ids, search_res.ids)
search_res = self.env['crm.lead'].search(_domain, limit=4, offset=3, order=_order)
self.assertEqual(expected_leads_asc[3:7].ids, search_res.ids)
search_res = self.env['crm.lead'].search(_domain, limit=None, offset=3, order=_order)
self.assertEqual(expected_leads_asc[3:].ids, search_res.ids)
_order = 'my_activity_date_deadline DESC, %s' % default_order
search_res = self.env['crm.lead'].search(_domain, limit=None, offset=0, order=_order)
self.assertEqual(expected_leads_desc.ids, search_res.ids)
search_res = self.env['crm.lead'].search(_domain, limit=4, offset=0, order=_order)
self.assertEqual(expected_leads_desc[:4].ids, search_res.ids)
search_res = self.env['crm.lead'].search(_domain, limit=4, offset=3, order=_order)
self.assertEqual(expected_leads_desc[3:7].ids, search_res.ids)
search_res = self.env['crm.lead'].search(_domain, limit=None, offset=3, order=_order)
self.assertEqual(expected_leads_desc[3:].ids, search_res.ids)
def test_crm_activity_recipients(self):
""" This test case checks
- no internal subtype followed by client
- activity subtype are not default ones
- only activity followers are recipients when this kind of activity is logged
"""
# Add explicitly a the client as follower
self.lead_1.message_subscribe(partner_ids=[self.contact_1.id])
# Check the client is not follower of any internal subtype
internal_subtypes = self.lead_1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.contact_1).mapped('subtype_ids').filtered(lambda subtype: subtype.internal)
self.assertFalse(internal_subtypes)
# Add sale manager as follower of default subtypes
self.lead_1.message_subscribe(partner_ids=[self.user_sales_manager.partner_id.id], subtype_ids=[self.env.ref('mail.mt_activities').id, self.env.ref('mail.mt_comment').id])
activity = self.env['mail.activity'].with_user(self.user_sales_leads).create({
'activity_type_id': self.activity_type_1.id,
'note': 'Content of the activity to log',
'res_id': self.lead_1.id,
'res_model_id': self.env.ref('crm.model_crm_lead').id,
})
activity._onchange_activity_type_id()
self.assertEqual(self.lead_1.activity_type_id, self.activity_type_1)
self.assertEqual(self.lead_1.activity_summary, self.activity_type_1.summary)
# self.assertEqual(self.lead.activity_date_deadline, self.activity_type_1.summary)
# mark as done, check lead and posted message
activity.action_done()
self.assertFalse(self.lead_1.activity_type_id.id)
self.assertFalse(self.lead_1.activity_ids)
activity_message = self.lead_1.message_ids[0]
self.assertEqual(activity_message.notified_partner_ids, self.user_sales_manager.partner_id)
self.assertEqual(activity_message.subtype_id, self.env.ref('mail.mt_activities'))
def test_crm_activity_next_action(self):
""" This test case set the next activity on a lead, log another, and schedule a third. """
# Add the next activity (like we set it from a form view)
lead_model_id = self.env['ir.model']._get('crm.lead').id
activity = self.env['mail.activity'].with_user(self.user_sales_manager).create({
'activity_type_id': self.activity_type_1.id,
'summary': 'My Own Summary',
'res_id': self.lead_1.id,
'res_model_id': lead_model_id,
})
activity._onchange_activity_type_id()
# Check the next activity is correct
self.assertEqual(self.lead_1.activity_summary, activity.summary)
self.assertEqual(self.lead_1.activity_type_id, activity.activity_type_id)
# self.assertEqual(fields.Datetime.from_string(self.lead.activity_date_deadline), datetime.now() + timedelta(days=activity.activity_type_id.days))
activity.write({
'activity_type_id': self.activity_type_2.id,
'summary': '',
'note': 'Content of the activity to log',
})
activity._onchange_activity_type_id()
self.assertEqual(self.lead_1.activity_summary, activity.activity_type_id.summary)
self.assertEqual(self.lead_1.activity_type_id, activity.activity_type_id)
# self.assertEqual(fields.Datetime.from_string(self.lead.activity_date_deadline), datetime.now() + timedelta(days=activity.activity_type_id.days))
activity.action_done()
# Check the next activity on the lead has been removed
self.assertFalse(self.lead_1.activity_type_id)

View file

@ -0,0 +1,986 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from freezegun import freeze_time
from unittest.mock import patch
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.phone_validation.tools.phone_validation import phone_format
from odoo.exceptions import UserError
from odoo.tests.common import Form, tagged, users
from odoo.tools import mute_logger
@tagged('lead_internals')
class TestCRMLead(TestCrmCommon):
@classmethod
def setUpClass(cls):
super(TestCRMLead, cls).setUpClass()
cls.country_ref = cls.env.ref('base.be')
cls.test_email = '"Test Email" <test.email@example.com>'
cls.test_phone = '0485112233'
def assertLeadAddress(self, lead, street, street2, city, lead_zip, state, country):
self.assertEqual(lead.street, street)
self.assertEqual(lead.street2, street2)
self.assertEqual(lead.city, city)
self.assertEqual(lead.zip, lead_zip)
self.assertEqual(lead.state_id, state)
self.assertEqual(lead.country_id, country)
@users('user_sales_leads')
def test_crm_lead_contact_fields_mixed(self):
""" Test mixed configuration from partner: both user input and coming
from partner, in order to ensure we do not loose information or make
it incoherent. """
lead_data = {
'name': 'TestMixed',
'partner_id': self.contact_1.id,
# address
'country_id': self.country_ref.id,
# other contact fields
'function': 'Parmesan Rappeur',
'lang_id': self.lang_fr.id,
# specific contact fields
'email_from': self.test_email,
'phone': self.test_phone,
}
lead = self.env['crm.lead'].create(lead_data)
# classic
self.assertEqual(lead.name, "TestMixed")
# address
self.assertLeadAddress(lead, False, False, False, False, self.env['res.country.state'], self.country_ref)
# other contact fields
for fname in set(PARTNER_FIELDS_TO_SYNC) - set(['function', 'lang']):
self.assertEqual(lead[fname], self.contact_1[fname], 'No user input -> take from contact for field %s' % fname)
self.assertEqual(lead.function, 'Parmesan Rappeur', 'User input should take over partner value')
self.assertEqual(lead.lang_id, self.lang_fr)
# specific contact fields
self.assertEqual(lead.partner_name, self.contact_company_1.name)
self.assertEqual(lead.contact_name, self.contact_1.name)
self.assertEqual(lead.email_from, self.test_email)
self.assertEqual(lead.phone, self.test_phone)
# update a single address fields -> only those are updated
lead.write({'street': 'Super Street', 'city': 'Super City'})
self.assertLeadAddress(lead, 'Super Street', False, 'Super City', False, self.env['res.country.state'], self.country_ref)
# change partner -> whole address updated
lead.write({'partner_id': self.contact_company_1.id})
for fname in PARTNER_ADDRESS_FIELDS_TO_SYNC:
self.assertEqual(lead[fname], self.contact_company_1[fname])
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_creation_no_partner(self):
lead_data = {
'name': 'Test',
'country_id': self.country_ref.id,
'email_from': self.test_email,
'phone': self.test_phone,
}
lead = self.env['crm.lead'].new(lead_data)
# get the street should not trigger cache miss
lead.street
# Create the lead and the write partner_id = False: country should remain
lead = self.env['crm.lead'].create(lead_data)
self.assertEqual(lead.country_id, self.country_ref, "Country should be set on the lead")
self.assertEqual(lead.email_from, self.test_email)
self.assertEqual(lead.phone, self.test_phone)
lead.partner_id = False
self.assertEqual(lead.country_id, self.country_ref, "Country should still be set on the lead")
self.assertEqual(lead.email_from, self.test_email)
self.assertEqual(lead.phone, self.test_phone)
@users('user_sales_manager')
def test_crm_lead_creation_partner(self):
lead = self.env['crm.lead'].create({
'name': 'TestLead',
'contact_name': 'Raoulette TestContact',
'email_from': '"Raoulette TestContact" <raoulette@test.example.com>',
})
self.assertEqual(lead.type, 'lead')
self.assertEqual(lead.user_id, self.user_sales_manager)
self.assertEqual(lead.team_id, self.sales_team_1)
self.assertEqual(lead.stage_id, self.stage_team1_1)
self.assertEqual(lead.contact_name, 'Raoulette TestContact')
self.assertEqual(lead.email_from, '"Raoulette TestContact" <raoulette@test.example.com>')
self.assertEqual(self.contact_1.lang, self.env.user.lang)
# update to a partner, should udpate address
lead.write({'partner_id': self.contact_1.id})
self.assertEqual(lead.partner_name, self.contact_company_1.name)
self.assertEqual(lead.contact_name, self.contact_1.name)
self.assertEqual(lead.email_from, self.contact_1.email)
self.assertEqual(lead.street, self.contact_1.street)
self.assertEqual(lead.city, self.contact_1.city)
self.assertEqual(lead.zip, self.contact_1.zip)
self.assertEqual(lead.country_id, self.contact_1.country_id)
self.assertEqual(lead.lang_id.code, self.contact_1.lang)
@users('user_sales_manager')
def test_crm_lead_creation_partner_address(self):
""" Test that an address erases all lead address fields (avoid mixed addresses) """
other_country = self.env.ref('base.fr')
empty_partner = self.env['res.partner'].create({
'name': 'Empty partner',
'country_id': other_country.id,
})
lead_data = {
'name': 'Test',
'street': 'My street',
'street2': 'My street',
'city': 'My city',
'zip': 'test@odoo.com',
'state_id': self.env['res.country.state'].create({
'name': 'My state',
'country_id': self.country_ref.id,
'code': 'MST',
}).id,
'country_id': self.country_ref.id,
}
lead = self.env['crm.lead'].create(lead_data)
lead.partner_id = empty_partner
# PARTNER_ADDRESS_FIELDS_TO_SYNC
self.assertEqual(lead.street, empty_partner.street, "Street should be sync from the Partner")
self.assertEqual(lead.street2, empty_partner.street2, "Street 2 should be sync from the Partner")
self.assertEqual(lead.city, empty_partner.city, "City should be sync from the Partner")
self.assertEqual(lead.zip, empty_partner.zip, "Zip should be sync from the Partner")
self.assertEqual(lead.state_id, empty_partner.state_id, "State should be sync from the Partner")
self.assertEqual(lead.country_id, empty_partner.country_id, "Country should be sync from the Partner")
@users('user_sales_manager')
def test_crm_lead_creation_partner_company(self):
""" Test lead / partner synchronization involving company details """
# Test that partner_name (company name) is the partner name if partner is company
lead = self.env['crm.lead'].create({
'name': 'TestLead',
'partner_id': self.contact_company.id,
})
self.assertEqual(lead.contact_name, False,
"Lead contact name should be Falsy when dealing with companies")
self.assertEqual(lead.partner_name, self.contact_company.name,
"Lead company name should be set to partner name if partner is a company")
# Test that partner_name (company name) is the partner company name if partner is an individual
self.contact_company.write({'is_company': False})
lead = self.env['crm.lead'].create({
'name': 'TestLead',
'partner_id': self.contact_company.id,
})
self.assertEqual(lead.contact_name, self.contact_company.name,
"Lead contact name should be set to partner name if partner is not a company")
self.assertEqual(lead.partner_name, self.contact_company.company_name,
"Lead company name should be set to company name if partner is not a company")
@users('user_sales_manager')
def test_crm_lead_creation_partner_no_address(self):
""" Test that an empty address on partner does not void its lead values """
empty_partner = self.env['res.partner'].create({
'name': 'Empty partner',
'is_company': True,
'lang': 'en_US',
'mobile': '123456789',
'title': self.env.ref('base.res_partner_title_mister').id,
'function': 'My function',
})
lead_data = {
'name': 'Test',
'contact_name': 'Test',
'street': 'My street',
'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)
lead.partner_id = empty_partner
# SPECIFIC FIELDS
self.assertEqual(lead.contact_name, lead_data['contact_name'], "Contact should remain")
self.assertEqual(lead.email_from, lead_data['email_from'], "Email From should keep its initial value")
self.assertEqual(lead.partner_name, empty_partner.name, "Partner name should be set as contact is a company")
# PARTNER_ADDRESS_FIELDS_TO_SYNC
self.assertEqual(lead.street, lead_data['street'], "Street should remain since partner has no address field set")
self.assertEqual(lead.street2, False, "Street2 should remain since partner has no address field set")
self.assertEqual(lead.country_id, self.country_ref, "Country should remain since partner has no address field set")
self.assertEqual(lead.city, False, "City should remain since partner has no address field set")
self.assertEqual(lead.zip, False, "Zip should remain since partner has no address field set")
self.assertEqual(lead.state_id, self.env['res.country.state'], "State should remain since partner has no address field set")
# 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")
@users('user_sales_manager')
def test_crm_lead_create_pipe_data(self):
""" Test creation pipe data: user, team, stage, depending on some default
configuration. """
# gateway-like creation: no user, no team, generic stage
lead = self.env['crm.lead'].with_context(default_user_id=False).create({
'name': 'Test',
'contact_name': 'Test Contact',
'email_from': self.test_email,
'phone': self.test_phone,
})
self.assertEqual(lead.user_id, self.env['res.users'])
self.assertEqual(lead.team_id, self.env['crm.team'])
self.assertEqual(lead.stage_id, self.stage_gen_1)
# pipe creation: current user's best team and default stage
lead = self.env['crm.lead'].create({
'name': 'Test',
'contact_name': 'Test Contact',
'email_from': self.test_email,
'phone': self.test_phone,
})
self.assertEqual(lead.user_id, self.user_sales_manager)
self.assertEqual(lead.team_id, self.sales_team_1)
self.assertEqual(lead.stage_id, self.stage_team1_1)
@users('user_sales_manager')
def test_crm_lead_currency_sync(self):
lead = self.env['crm.lead'].create({
'name': 'Lead 1',
'company_id': self.company_main.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})
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):
# Test for one won lead
stage_team1_won2 = self.env['crm.stage'].create({
'name': 'Won2',
'sequence': 75,
'team_id': self.sales_team_1.id,
'is_won': True,
})
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
self.assertTrue(won_lead.date_closed)
self.assertFalse(other_lead.date_closed)
# multi update
leads = won_lead + other_lead
with freeze_time('2020-02-02 18:00'):
leads.stage_id = stage_team1_won2
self.assertEqual(won_lead.date_closed, old_date_closed, 'Should not change date')
self.assertEqual(other_lead.date_closed, datetime(2020, 2, 2, 18, 0, 0))
# back to open stage
leads.write({'stage_id': self.stage_team1_2.id})
self.assertFalse(won_lead.date_closed)
self.assertFalse(other_lead.date_closed)
# close with lost
with freeze_time('2020-02-02 18:00'):
leads.action_set_lost()
self.assertEqual(won_lead.date_closed, datetime(2020, 2, 2, 18, 0, 0))
self.assertEqual(other_lead.date_closed, datetime(2020, 2, 2, 18, 0, 0))
@users('user_sales_leads')
@freeze_time("2012-01-14")
def test_crm_lead_lost_date_closed(self):
lead = self.lead_1.with_env(self.env)
self.assertFalse(lead.date_closed, "Initially, closed date is not set")
# Mark the lead as lost
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_partner_sync(self):
lead, partner = self.lead_1.with_user(self.env.user), self.contact_2
partner_email, partner_phone = self.contact_2.email, self.contact_2.phone
lead.partner_id = partner
# email & phone must be automatically set on the lead
lead.partner_id = partner
self.assertEqual(lead.email_from, partner_email)
self.assertEqual(lead.phone, partner_phone)
# writing on the lead field must change the partner field
lead.email_from = '"John Zoidberg" <john.zoidberg@test.example.com>'
lead.phone = '+1 202 555 7799'
self.assertEqual(partner.email, '"John Zoidberg" <john.zoidberg@test.example.com>')
self.assertEqual(partner.email_normalized, 'john.zoidberg@test.example.com')
self.assertEqual(partner.phone, '+1 202 555 7799')
# writing on the partner must change the lead values
partner.email = partner_email
partner.phone = '+1 202 555 6666'
self.assertEqual(lead.email_from, partner_email)
self.assertEqual(lead.phone, '+1 202 555 6666')
# resetting lead values also resets partner
lead.email_from, lead.phone = False, False
self.assertFalse(partner.email)
self.assertFalse(partner.email_normalized)
self.assertFalse(partner.phone)
@users('user_sales_manager')
def test_crm_lead_partner_sync_email_phone(self):
""" Specifically test synchronize between a lead and its partner about
phone and email fields. Phone especially has some corner cases due to
automatic formatting (notably with onchange in form view). """
lead, partner = self.lead_1.with_user(self.env.user), self.contact_2
# This is a type == 'lead', not a type == 'opportunity'
# {'invisible': ['|', ('type', '=', 'opportunity'), ('is_partner_visible', '=', False)]}
# lead.is_partner_visible = bool(lead.type == 'opportunity' or lead.partner_id or is_debug_mode)
# Hence, debug mode required for `partner_id` to be visible
with self.debug_mode():
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_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
# as well as mobile (who does not trigger the reverse sync)
lead_form.partner_id = partner
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)
lead_form.save()
self.assertEqual(partner.phone, partner_phone,
'Lead / Partner: partner values sent to lead')
self.assertEqual(lead.email_from, partner_email,
'Lead / Partner: partner values sent to lead')
self.assertEqual(lead.email_normalized, partner_email_normalized,
'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,
'Lead: phone_sanitized computed field on mobile')
# for email_from, if only formatting differs, warning should not appear and
# email on partner should not be updated
lead_form.email_from = '"Hermes Conrad" <%s>' % partner_email_normalized
self.assertFalse(lead_form.partner_email_update)
lead_form.save()
self.assertEqual(partner.email, partner_email)
# for phone, if only formatting differs, warning should not appear and
# phone on partner should not be updated
lead_form.phone = partner_phone_sanitized
self.assertFalse(lead_form.partner_phone_update)
lead_form.save()
self.assertEqual(partner.phone, partner_phone)
# LEAD/PARTNER SYNC: lead updates partner
new_email = '"John Zoidberg" <john.zoidberg@test.example.com>'
new_email_normalized = 'john.zoidberg@test.example.com'
lead_form.email_from = new_email
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")
lead_form.phone = new_phone
self.assertEqual(lead_form.phone, new_phone_formatted)
self.assertTrue(lead_form.partner_email_update)
self.assertTrue(lead_form.partner_phone_update)
lead_form.save()
self.assertEqual(partner.email, new_email)
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_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.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,
'Partner sanitized should be computed on mobile')
@users('user_sales_manager')
def test_crm_lead_partner_sync_email_phone_corner_cases(self):
""" Test corner cases of email and phone sync (False versus '', formatting
differences, wrong input, ...) """
test_email = 'amy.wong@test.example.com'
lead = self.lead_1.with_user(self.env.user)
lead.write({'phone': False}) # reset phone to start with all Falsy values
contact = self.env['res.partner'].create({
'name': 'NoContact Partner',
'phone': '',
'email': '',
'mobile': '',
})
# This is a type == 'lead', not a type == 'opportunity'
# {'invisible': ['|', ('type', '=', 'opportunity'), ('is_partner_visible', '=', False)]}
# lead.is_partner_visible = bool(lead.type == 'opportunity' or lead.partner_id or is_debug_mode)
# Hence, debug mode required for `partner_id` to be visible
with self.debug_mode():
lead_form = Form(lead)
self.assertEqual(lead_form.email_from, test_email)
self.assertFalse(lead_form.partner_email_update)
self.assertFalse(lead_form.partner_phone_update)
# email: False versus empty string
lead_form.partner_id = contact
self.assertTrue(lead_form.partner_email_update)
self.assertFalse(lead_form.partner_phone_update)
lead_form.email_from = ''
self.assertFalse(lead_form.partner_email_update)
lead_form.email_from = False
self.assertFalse(lead_form.partner_email_update)
# phone: False versus empty string
lead_form.phone = '+1 202-555-0888'
self.assertFalse(lead_form.partner_email_update)
self.assertTrue(lead_form.partner_phone_update)
lead_form.phone = ''
self.assertFalse(lead_form.partner_phone_update)
lead_form.phone = False
self.assertFalse(lead_form.partner_phone_update)
# email/phone: formatting should not trigger ribbon
lead.write({
'email_from': '"My Name" <%s>' % test_email,
'phone': '+1 202-555-0888',
})
contact.write({
'email': '"My Name" <%s>' % test_email,
'phone': '+1 202-555-0888',
})
lead_form = Form(lead)
self.assertFalse(lead_form.partner_email_update)
self.assertFalse(lead_form.partner_phone_update)
lead_form.partner_id = contact
self.assertFalse(lead_form.partner_email_update)
self.assertFalse(lead_form.partner_phone_update)
lead_form.email_from = '"Another Name" <%s>' % test_email # same email normalized
self.assertFalse(lead_form.partner_email_update, 'Formatting-only change should not trigger write')
self.assertFalse(lead_form.partner_phone_update, 'Formatting-only change should not trigger write')
lead_form.phone = '2025550888' # same number but another format
self.assertFalse(lead_form.partner_email_update, 'Formatting-only change should not trigger write')
self.assertFalse(lead_form.partner_phone_update, 'Formatting-only change should not trigger write')
# wrong value are also propagated
lead_form.phone = '666 789456789456789456'
self.assertTrue(lead_form.partner_phone_update)
# test country propagation allowing to correctly compute sanitized numbers
# by adding missing relevant information from contact
be_country = self.env.ref('base.be')
contact.write({
'country_id': be_country.id,
'phone': '+32456001122',
})
lead.write({'country_id': False})
lead_form = Form(lead)
lead_form.partner_id = contact
lead_form.phone = '0456 00 11 22'
self.assertFalse(lead_form.partner_phone_update)
self.assertEqual(lead_form.country_id, be_country)
@users('user_sales_manager')
def test_crm_lead_stages(self):
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 = 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)
self.assertEqual(lead.stage_id, self.stage_gen_won) # generic won stage has lower sequence than team won stage
@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
event form view) when lead is unlinked """
lead = self.env['crm.lead'].create({'name': 'Lead With Meetings'})
meetings = self.env['calendar.event'].create([
{
'name': 'Meeting 1 of Lead',
'res_id': lead.id,
'res_model_id': self.env['ir.model']._get_id(lead._name),
'start': '2022-07-12 08:00:00',
'stop': '2022-07-12 10:00:00',
}, {
'name': 'Meeting 2 of Lead',
'opportunity_id': lead.id,
'res_id': lead.id,
'res_model_id': self.env['ir.model']._get_id(lead._name),
'start': '2022-07-13 08:00:00',
'stop': '2022-07-13 10:00:00',
}
])
self.assertEqual(lead.calendar_event_count, 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'])
lead.unlink()
self.assertEqual(meetings.exists(), meetings)
self.assertFalse(meetings.opportunity_id)
self.assertEqual(set(meetings.mapped('res_id')), set([0]))
self.assertEqual(set(meetings.mapped('res_model')), set([False]))
@users('user_sales_leads')
def test_crm_lead_update_contact(self):
# ensure initial data, especially for corner cases
self.assertFalse(self.contact_company_1.phone)
self.assertEqual(self.contact_company_1.country_id.code, "US")
lead = self.env['crm.lead'].create({
'name': 'Test',
'country_id': self.country_ref.id,
'email_from': self.test_email,
'phone': self.test_phone,
})
self.assertEqual(lead.country_id, self.country_ref, "Country should be set on the lead")
lead.partner_id = False
self.assertEqual(lead.country_id, self.country_ref, "Country should still be set on the lead")
self.assertEqual(lead.email_from, self.test_email)
self.assertEqual(lead.phone, self.test_phone)
self.assertEqual(lead.email_state, 'correct')
self.assertEqual(lead.phone_state, 'correct')
lead.partner_id = self.contact_company_1
self.assertEqual(lead.country_id, self.contact_company_1.country_id, "Country should still be the one set on partner")
self.assertEqual(lead.email_from, self.contact_company_1.email)
self.assertEqual(lead.phone, self.test_phone)
self.assertEqual(lead.email_state, 'correct')
# currently we keep phone as partner as a void one -> may lead to inconsistencies
self.assertEqual(lead.phone_state, 'incorrect', "Belgian phone with US country -> considered as incorrect")
lead.email_from = 'broken'
lead.phone = 'alsobroken'
self.assertEqual(lead.email_state, 'incorrect')
self.assertEqual(lead.phone_state, 'incorrect')
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({
'name': 'TestAlias',
'use_leads': True,
'use_opportunities': True,
'alias_name': 'test.alias'
})
self.assertEqual(new_team.alias_id.alias_name, 'test.alias')
self.assertEqual(new_team.alias_name, 'test.alias')
new_team.write({
'use_leads': False,
'use_opportunities': False,
})
# self.assertFalse(new_team.alias_id.alias_name)
# self.assertFalse(new_team.alias_name)
@users('user_sales_manager')
def test_crm_team_alias_helper(self):
"""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._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',
'use_leads': True,
'member_ids': [(6, 0, [self.env.user.id])],
}, {
'name': 'TeamLeads',
'use_leads': True,
'member_ids': [],
}, {
'name': 'UserTeamOpportunities',
'use_leads': False,
'member_ids': [(6, 0, [self.env.user.id])],
}, {
'name': 'TeamOpportunities',
'use_leads': False,
'member_ids': [],
}])
self.env['crm.lead'].create([{
'name': 'LeadOurTeam',
'team_id': user_team_leads.id,
'type': 'lead',
}, {
'name': 'LeadTeam',
'team_id': team_leads.id,
'type': 'lead',
}, {
'name': 'OpportunityOurTeam',
'team_id': user_team_opport.id,
'type': 'opportunity',
}, {
'name': 'OpportunityTeam',
'team_id': team_opport.id,
'type': 'opportunity',
}])
self.env['crm.lead'].with_user(self.user_sales_manager_mc).create({
'name': 'LeadOtherComp',
'team_id': team_other_comp.id,
'type': 'lead',
'company_id': self.company_2.id,
})
# archive our team one by one and check that we have the correct help message
teams = [user_team_leads, team_leads, user_team_opport, team_opport, team_other_comp]
for team in teams:
team.alias_id.sudo().write({'alias_name': team.name})
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(""))
else:
self.assertNotIn(f"<a href='mailto:{team_mail}'>{team_mail}</a>", self.env['crm.lead'].sudo().get_empty_list_help(""))
team.active = False
@mute_logger('odoo.addons.mail.models.mail_thread')
def test_mailgateway(self):
new_lead = self.format_and_process(
INCOMING_EMAIL,
'unknown.sender@test.example.com',
'%s@%s' % (self.sales_team_1.alias_name, self.alias_domain),
subject='Delivery cost inquiry',
target_model='crm.lead',
)
self.assertEqual(new_lead.email_from, 'unknown.sender@test.example.com')
self.assertFalse(new_lead.partner_id)
self.assertEqual(new_lead.name, 'Delivery cost inquiry')
message = new_lead.with_user(self.user_sales_manager).message_post(
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):
lead_1 = self.env['crm.lead'].create({
'name': 'Lead 1',
'country_id': self.env.ref('base.be').id,
'phone': '+32485001122',
})
lead_2 = self.env['crm.lead'].create({
'name': 'Lead 2',
'country_id': self.env.ref('base.be').id,
'phone': '0032485001122',
})
lead_3 = self.env['crm.lead'].create({
'name': 'Lead 3',
'country_id': self.env.ref('base.be').id,
'phone': 'hello',
})
lead_4 = self.env['crm.lead'].create({
'name': 'Lead 3',
'country_id': self.env.ref('base.be').id,
'phone': '+32485112233',
})
# search term containing less than 3 characters should throw an error (some currently not working)
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 ')])
with self.assertRaises(UserError):
self.env['crm.lead'].search([('phone_mobile_search', 'like', 'c')])
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', '5')])
with self.assertRaises(UserError):
self.env['crm.lead'].search([('phone_mobile_search', 'like', '42')])
# + / 00 prefixes do not block search
self.assertEqual(lead_1 + lead_2, self.env['crm.lead'].search([
('phone_mobile_search', 'like', '+32485001122')
]))
self.assertEqual(lead_1 + lead_2, self.env['crm.lead'].search([
('phone_mobile_search', 'like', '0032485001122')
]))
self.assertEqual(lead_1 + lead_2, self.env['crm.lead'].search([
('phone_mobile_search', 'like', '485001122')
]))
self.assertEqual(lead_1 + lead_2 + lead_4, self.env['crm.lead'].search([
('phone_mobile_search', 'like', '1122')
]))
# textual input still possible
self.assertEqual(
self.env['crm.lead'].search([('phone_mobile_search', 'like', 'hello')]),
lead_3,
'Should behave like a text field'
)
self.assertFalse(
self.env['crm.lead'].search([('phone_mobile_search', 'like', 'Hello')]),
'Should behave like a text field (case sensitive)'
)
self.assertEqual(
self.env['crm.lead'].search([('phone_mobile_search', 'ilike', 'Hello')]),
lead_3,
'Should behave like a text field (case insensitive)'
)
self.assertEqual(
self.env['crm.lead'].search([('phone_mobile_search', 'like', 'hello123')]),
self.env['crm.lead'],
'Should behave like a text field'
)
@users('user_sales_manager')
def test_phone_mobile_search_format(self):
numbers = [
# standard
'0499223311',
# separators
'0499/223311', '0499/22.33.11', '0499/22 33 11', '0499/223 311',
# international format -> currently not working
# '+32499223311', '0032499223311',
]
leads = self.env['crm.lead'].create([
{'name': 'Lead %s' % index,
'country_id': self.env.ref('base.be').id,
'phone': number,
}
for index, number in enumerate(numbers)
])
self.assertEqual(leads, self.env['crm.lead'].search([
('phone_mobile_search', 'like', '0499223311')
]))
self.assertEqual(leads, self.env['crm.lead'].search([
('phone_mobile_search', 'like', '0499/223311')
]))
self.assertEqual(leads, self.env['crm.lead'].search([
('phone_mobile_search', 'like', '0499/22.33.11')
]))
self.assertEqual(leads, self.env['crm.lead'].search([
('phone_mobile_search', 'like', '0499/22 33 11')
]))
self.assertEqual(leads, self.env['crm.lead'].search([
('phone_mobile_search', 'like', '0499/223 311')
]))
@users('user_sales_manager')
def test_phone_mobile_update(self):
lead = self.env['crm.lead'].create({
'name': 'Lead 1',
'country_id': self.env.ref('base.us').id,
'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]})
self.assertFalse(lead.phone)
self.assertEqual(lead.mobile, self.test_phone_data[1])
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[1])
lead.write({'phone': self.test_phone_data[1], 'mobile': self.test_phone_data[2]})
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])
# 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):
def test_address_view(self):
self.env.company.country_id = self.env.ref('base.us')
self.assertAddressView('crm.lead')

View file

@ -0,0 +1,565 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import random
from datetime import datetime
from dateutil.relativedelta import relativedelta
from unittest.mock import patch
from odoo import fields
from odoo.addons.crm.tests.common import TestLeadConvertCommon
from odoo.tests.common import tagged
from odoo.tools import mute_logger
class TestLeadAssignCommon(TestLeadConvertCommon):
@classmethod
def setUpClass(cls):
super(TestLeadAssignCommon, cls).setUpClass()
cls._switch_to_multi_membership()
cls._switch_to_auto_assign()
# don't mess with existing teams, deactivate them to make tests repeatable
cls.sales_teams = cls.sales_team_1 + cls.sales_team_convert
cls.members = cls.sales_team_1_m1 + cls.sales_team_1_m2 + cls.sales_team_1_m3 + cls.sales_team_convert_m1 + cls.sales_team_convert_m2
cls.env['crm.team'].search([('id', 'not in', cls.sales_teams.ids)]).write({'active': False})
# don't mess with existing leads, unlink those assigned to users used here to make tests
# repeatable (archive is not sufficient because of lost leads)
with mute_logger('odoo.models.unlink'):
cls.env['crm.lead'].with_context(active_test=False).search(['|', ('team_id', '=', False), ('user_id', 'in', cls.sales_teams.member_ids.ids)]).unlink()
cls.bundle_size = 50
cls.env['ir.config_parameter'].set_param('crm.assignment.commit.bundle', '%s' % cls.bundle_size)
cls.env['ir.config_parameter'].set_param('crm.assignment.delay', '0')
def assertInitialData(self):
self.assertEqual(self.sales_team_1.assignment_max, 75)
self.assertEqual(self.sales_team_convert.assignment_max, 90)
# ensure domains
self.assertEqual(self.sales_team_1.assignment_domain, False)
self.assertEqual(self.sales_team_1_m1.assignment_domain, False)
self.assertEqual(self.sales_team_1_m2.assignment_domain, "[('probability', '>=', 10)]")
self.assertEqual(self.sales_team_1_m3.assignment_domain, "[('probability', '>=', 20)]")
self.assertEqual(self.sales_team_convert.assignment_domain, "[('priority', 'in', ['1', '2', '3'])]")
self.assertEqual(self.sales_team_convert_m1.assignment_domain, "[('probability', '>=', 20)]")
self.assertEqual(self.sales_team_convert_m2.assignment_domain, False)
# start afresh
self.assertEqual(self.sales_team_1_m1.lead_month_count, 0)
self.assertEqual(self.sales_team_1_m2.lead_month_count, 0)
self.assertEqual(self.sales_team_1_m3.lead_month_count, 0)
self.assertEqual(self.sales_team_convert_m1.lead_month_count, 0)
self.assertEqual(self.sales_team_convert_m2.lead_month_count, 0)
@tagged('lead_assign')
class TestLeadAssign(TestLeadAssignCommon):
""" Test lead assignment feature added in saas-14.2 """
def test_assign_configuration(self):
now_patch = datetime(2020, 11, 2, 10, 0, 0)
with patch.object(fields.Datetime, 'now', return_value=now_patch):
config = self.env['res.config.settings'].create({
'crm_use_auto_assignment': True,
'crm_auto_assignment_action': 'auto',
'crm_auto_assignment_interval_number': 19,
'crm_auto_assignment_interval_type': 'hours'
})
config._onchange_crm_auto_assignment_run_datetime()
config.execute()
self.assertTrue(self.assign_cron.active)
self.assertEqual(self.assign_cron.nextcall, datetime(2020, 11, 2, 10, 0, 0) + relativedelta(hours=19))
config.write({
'crm_auto_assignment_interval_number': 2,
'crm_auto_assignment_interval_type': 'days'
})
config._onchange_crm_auto_assignment_run_datetime()
config.execute()
self.assertTrue(self.assign_cron.active)
self.assertEqual(self.assign_cron.nextcall, datetime(2020, 11, 2, 10, 0, 0) + relativedelta(days=2))
config.write({
'crm_auto_assignment_run_datetime': fields.Datetime.to_string(datetime(2020, 11, 1, 10, 0, 0)),
})
config.execute()
self.assertTrue(self.assign_cron.active)
self.assertEqual(self.assign_cron.nextcall, datetime(2020, 11, 1, 10, 0, 0))
config.write({
'crm_auto_assignment_action': 'manual',
})
config.execute()
self.assertFalse(self.assign_cron.active)
self.assertEqual(self.assign_cron.nextcall, datetime(2020, 11, 1, 10, 0, 0))
config.write({
'crm_use_auto_assignment': False,
'crm_auto_assignment_action': 'auto',
})
config.execute()
self.assertFalse(self.assign_cron.active)
self.assertEqual(self.assign_cron.nextcall, datetime(2020, 11, 1, 10, 0, 0))
def test_assign_count(self):
""" Test number of assigned leads when dealing with some existing data (leads
or opportunities) as well as with opt-out management. """
leads = self._create_leads_batch(
lead_type='lead',
user_ids=[False],
partner_ids=[False, False, False, self.contact_1.id],
probabilities=[30],
count=8,
suffix='Initial',
)
# commit probability and related fields
leads.flush_recordset()
self.assertInitialData()
# archived members should not be taken into account
self.sales_team_1_m1.action_archive()
# assignment_max = 0 means opt_out
self.sales_team_1_m2.assignment_max = 0
# assign probability to leads (bypass auto probability as purpose is not to test pls)
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
for idx, lead in enumerate(leads):
lead.probability = idx * 10
# commit probability and related fields
leads.flush_recordset()
self.assertEqual(leads[0].probability, 0)
# create exiting leads for user_sales_salesman (sales_team_1_m3, sales_team_convert_m1)
existing_leads = self._create_leads_batch(
lead_type='lead', user_ids=[self.user_sales_salesman.id],
probabilities=[10],
count=14,
suffix='Existing')
self.assertEqual(existing_leads.team_id, self.sales_team_1, "Team should have lower sequence")
existing_leads[0].active = False # lost
existing_leads[1].probability = 100 # not won
existing_leads[2].probability = 0 # not lost
existing_leads.flush_recordset()
self.members.invalidate_model(['lead_month_count'])
self.assertEqual(self.sales_team_1_m3.lead_month_count, 14)
self.assertEqual(self.sales_team_convert_m1.lead_month_count, 0)
# re-assign existing leads, check monthly count is updated
existing_leads[-2:]._handle_salesmen_assignment(user_ids=self.user_sales_manager.ids)
# commit probability and related fields
leads.flush_recordset()
self.members.invalidate_model(['lead_month_count'])
self.assertEqual(self.sales_team_1_m3.lead_month_count, 12)
# sales_team_1_m2 is opt-out (new field in 14.3) -> even with max, no lead assigned
self.sales_team_1_m2.update({'assignment_max': 45, 'assignment_optout': True})
with self.with_user('user_sales_manager'):
teams_data, members_data = self.sales_team_1._action_assign_leads(work_days=4)
Leads = self.env['crm.lead']
self.assertEqual(
sorted(Leads.browse(teams_data[self.sales_team_1]['assigned']).mapped('name')),
['TestLeadInitial_0000', 'TestLeadInitial_0001', 'TestLeadInitial_0002',
'TestLeadInitial_0004', 'TestLeadInitial_0005', 'TestLeadInitial_0006']
)
self.assertEqual(
Leads.browse(teams_data[self.sales_team_1]['merged']).mapped('name'),
['TestLeadInitial_0003']
)
self.assertEqual(len(teams_data[self.sales_team_1]['duplicates']), 1)
self.assertEqual(
sorted(members_data[self.sales_team_1_m3]['assigned'].mapped('name')),
['TestLeadInitial_0000', 'TestLeadInitial_0005']
)
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertEqual(self.sales_team_1_m1.lead_month_count, 0) # archived do not get leads
self.assertEqual(self.sales_team_1_m2.lead_month_count, 0) # opt-out through assignment_max = 0
self.assertEqual(self.sales_team_1_m3.lead_month_count, 14) # 15 max on 4 days (2) + existing 12
with self.with_user('user_sales_manager'):
self.env['crm.team'].browse(self.sales_team_1.ids)._action_assign_leads(work_days=4)
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertEqual(self.sales_team_1_m1.lead_month_count, 0) # archived do not get leads
self.assertEqual(self.sales_team_1_m2.lead_month_count, 0) # opt-out through assignment_max = 0
self.assertEqual(self.sales_team_1_m3.lead_month_count, 16) # 15 max on 4 days (2) + existing 14 and not capped anymore
@mute_logger('odoo.models.unlink')
def test_assign_duplicates(self):
""" Test assign process with duplicates on partner. Allow to ensure notably
that de duplication is effectively performed. """
# fix the seed and avoid randomness
random.seed(1940)
leads = self._create_leads_batch(
lead_type='lead',
user_ids=[False],
partner_ids=[self.contact_1.id, self.contact_2.id, False, False, False],
count=200
)
# commit probability and related fields
leads.flush_recordset()
self.assertInitialData()
# assign probability to leads (bypass auto probability as purpose is not to test pls)
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
for idx in range(0, 5):
sliced_leads = leads[idx:len(leads):5]
for lead in sliced_leads:
lead.probability = (idx + 1) * 10 * ((int(lead.priority) + 1) / 2)
# commit probability and related fields
leads.flush_recordset()
with self.with_user('user_sales_manager'):
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2)
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
self.assertEqual(len(leads), 122)
leads_st1 = leads.filtered_domain([('team_id', '=', self.sales_team_1.id)])
leads_stc = leads.filtered_domain([('team_id', '=', self.sales_team_convert.id)])
# check random globally assigned enough leads to team
self.assertEqual(len(leads_st1), 76)
self.assertEqual(len(leads_stc), 46)
self.assertEqual(len(leads_st1) + len(leads_stc), len(leads)) # Make sure all lead are assigned
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 11) # 45 max on 2 days (3) + compensation (8.4)
self.assertMemberAssign(self.sales_team_1_m2, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_1_m3, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_convert_m1, 8) # 30 max on 15 (2) + compensation (5.6)
self.assertMemberAssign(self.sales_team_convert_m2, 15) # 60 max on 15 (4) + compsantion (11.2)
# teams assign: everything should be done due to duplicates
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
self.assertEqual(len(leads.filtered_domain([('team_id', '=', False)])), 0)
# deduplicate should have removed all duplicated linked to contact_1 and contact_2
new_assigned_leads_wpartner = self.env['crm.lead'].search([
('partner_id', 'in', (self.contact_1 | self.contact_2).ids),
('id', 'in', leads.ids)
])
self.assertEqual(len(new_assigned_leads_wpartner), 2)
@mute_logger('odoo.models.unlink')
def test_assign_no_duplicates(self):
# fix the seed and avoid randomness
random.seed(1945)
leads = self._create_leads_batch(
lead_type='lead',
user_ids=[False],
partner_ids=[False],
count=150
)
# commit probability and related fields
leads.flush_recordset()
self.assertInitialData()
# assign probability to leads (bypass auto probability as purpose is not to test pls)
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
for idx in range(0, 5):
sliced_leads = leads[idx:len(leads):5]
for lead in sliced_leads:
lead.probability = (idx + 1) * 10 * ((int(lead.priority) + 1) / 2)
# commit probability and related fields
leads.flush_recordset()
with self.with_user('user_sales_manager'):
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2)
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
leads_st1 = leads.filtered_domain([('team_id', '=', self.sales_team_1.id)])
leads_stc = leads.filtered_domain([('team_id', '=', self.sales_team_convert.id)])
self.assertEqual(len(leads), 150)
# check random globally assigned enough leads to team
self.assertEqual(len(leads_st1), 104)
self.assertEqual(len(leads_stc), 46)
self.assertEqual(len(leads_st1) + len(leads_stc), len(leads)) # Make sure all lead are assigned
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 11) # 45 max on 2 days (3) + compensation (8.4)
self.assertMemberAssign(self.sales_team_1_m2, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_1_m3, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_convert_m1, 8) # 30 max on 15 (2) + compensation (5.6)
self.assertMemberAssign(self.sales_team_convert_m2, 15) # 60 max on 15 (4) + compensation (11.2)
@mute_logger('odoo.models.unlink')
def test_assign_populated(self):
""" Test assignment on a more high volume oriented test set in order to
test more real life use cases. """
# fix the seed and avoid randomness (funny: try 1870)
random.seed(1871)
# create leads enough to assign one month of work
_lead_count, _email_dup_count, _partner_count = 600, 50, 150
leads = self._create_leads_batch(
lead_type='lead',
user_ids=[False],
partner_count=_partner_count,
country_ids=[self.env.ref('base.be').id, self.env.ref('base.fr').id, False],
count=_lead_count,
email_dup_count=_email_dup_count)
# commit probability and related fields
leads.flush_recordset()
self.assertInitialData()
# assign for one month, aka a lot
self.env.ref('crm.ir_cron_crm_lead_assign').write({'interval_type': 'days', 'interval_number': 30})
# create a third team
sales_team_3 = self.env['crm.team'].create({
'name': 'Sales Team 3',
'sequence': 15,
'alias_name': False,
'use_leads': True,
'use_opportunities': True,
'company_id': False,
'user_id': False,
'assignment_domain': [('country_id', '!=', False)],
})
sales_team_3_m1 = self.env['crm.team.member'].create({
'user_id': self.user_sales_manager.id,
'crm_team_id': sales_team_3.id,
'assignment_max': 60,
'assignment_domain': False,
})
sales_team_3_m2 = self.env['crm.team.member'].create({
'user_id': self.user_sales_leads.id,
'crm_team_id': sales_team_3.id,
'assignment_max': 60,
'assignment_domain': False,
})
sales_team_3_m3 = self.env['crm.team.member'].create({
'user_id': self.user_sales_salesman.id,
'crm_team_id': sales_team_3.id,
'assignment_max': 15,
'assignment_domain': [('probability', '>=', 10)],
})
sales_teams = self.sales_teams + sales_team_3
self.assertEqual(sum(team.assignment_max for team in sales_teams), 300)
self.assertEqual(len(leads), 650)
# assign probability to leads (bypass auto probability as purpose is not to test pls)
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
for idx in range(0, 5):
sliced_leads = leads[idx:len(leads):5]
for lead in sliced_leads:
lead.probability = (idx + 1) * 10 * ((int(lead.priority) + 1) / 2)
# commit probability and related fields
leads.flush_recordset()
with self.with_user('user_sales_manager'):
self.env['crm.team'].browse(sales_teams.ids)._action_assign_leads(work_days=30)
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)])
self.assertEqual(leads.team_id, sales_teams)
self.assertEqual(leads.user_id, sales_teams.member_ids)
self.assertEqual(len(leads), 600)
# check random globally assigned enough leads to team
leads_st1 = leads.filtered_domain([('team_id', '=', self.sales_team_1.id)])
leads_st2 = leads.filtered_domain([('team_id', '=', self.sales_team_convert.id)])
leads_st3 = leads.filtered_domain([('team_id', '=', sales_team_3.id)])
self.assertLessEqual(len(leads_st1), 225) # 75 * 600 / 300 * 1.5 (because random)
self.assertLessEqual(len(leads_st2), 270) # 90 * 600 / 300 * 1.5 (because random)
self.assertLessEqual(len(leads_st3), 405) # 135 * 600 / 300 * 1.5 (because random)
self.assertGreaterEqual(len(leads_st1), 75) # 75 * 600 / 300 * 0.5 (because random)
self.assertGreaterEqual(len(leads_st2), 90) # 90 * 600 / 300 * 0.5 (because random)
self.assertGreaterEqual(len(leads_st3), 135) # 135 * 600 / 300 * 0.5 (because random)
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 45) # 45 max on one month
self.assertMemberAssign(self.sales_team_1_m2, 15) # 15 max on one month
self.assertMemberAssign(self.sales_team_1_m3, 15) # 15 max on one month
self.assertMemberAssign(self.sales_team_convert_m1, 30) # 30 max on one month
self.assertMemberAssign(self.sales_team_convert_m2, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m1, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m2, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m3, 15) # 15 max on one month
def test_assign_quota(self):
""" Test quota computation """
self.assertInitialData()
# quota computation without existing leads
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=1),
10,
"Assignment quota: 45 max on 1 days -> 1.5, compensation (45-1.5)/5 -> 8.7"
)
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=2),
11,
"Assignment quota: 45 max on 2 days -> 3, compensation (45-3)/5 -> 8.4"
)
# quota should not exceed maximum
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=30),
45,
"Assignment quota: no compensation as exceeding monthly count"
)
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=60),
90,
"Assignment quota: no compensation and no limit anymore (do as asked)"
)
# create exiting leads for user_sales_leads (sales_team_1_m1)
existing_leads = self._create_leads_batch(
lead_type='lead', user_ids=[self.user_sales_leads.id],
probabilities=[10],
count=30)
self.assertEqual(existing_leads.team_id, self.sales_team_1, "Team should have lower sequence")
existing_leads.flush_recordset()
self.sales_team_1_m1.invalidate_model(['lead_month_count'])
self.assertEqual(self.sales_team_1_m1.lead_month_count, 30)
# quota computation with existing leads
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=1),
4,
"Assignment quota: 45 max on 1 days -> 1.5, compensation (45-30-1.5)/5 -> 2.7"
)
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=2),
5,
"Assignment quota: 45 max on 2 days -> 3, compensation (45-30-3)/5 -> 2.4"
)
# quota should not exceed maximum
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=30),
45,
"Assignment quota: no compensation and no limit anymore (do as asked even with 30 already assigned)"
)
self.assertEqual(
self.sales_team_1_m1._get_assignment_quota(work_days=60),
90,
"Assignment quota: no compensation and no limit anymore (do as asked even with 30 already assigned)"
)
def test_assign_specific_won_lost(self):
""" Test leads taken into account in assign process: won, lost, stage
configuration. """
leads = self._create_leads_batch(
lead_type='lead',
user_ids=[False],
partner_ids=[False, False, False, self.contact_1.id],
probabilities=[30],
count=6
)
leads[0].stage_id = self.stage_gen_won.id # is won -> should not be taken into account
leads[1].stage_id = False
leads[2].update({'stage_id': False, 'probability': 0})
leads[3].update({'stage_id': False, 'probability': False})
leads[4].active = False # is lost -> should not be taken into account
leads[5].update({'team_id': self.sales_team_convert.id, 'user_id': self.user_sales_manager.id}) # assigned lead should not be re-assigned
# commit probability and related fields
leads.flush_recordset()
with self.with_user('user_sales_manager'):
self.env['crm.team'].browse(self.sales_team_1.ids)._action_assign_leads(work_days=4)
self.assertEqual(leads[0].team_id, self.env['crm.team'], 'Won lead should not be assigned')
self.assertEqual(leads[0].user_id, self.env['res.users'], 'Won lead should not be assigned')
for lead in leads[1:4]:
self.assertIn(lead.user_id, self.sales_team_1.member_ids)
self.assertEqual(lead.team_id, self.sales_team_1)
self.assertEqual(leads[4].team_id, self.env['crm.team'], 'Lost lead should not be assigned')
self.assertEqual(leads[4].user_id, self.env['res.users'], 'Lost lead should not be assigned')
self.assertEqual(leads[5].team_id, self.sales_team_convert, 'Assigned lead should not be reassigned')
self.assertEqual(leads[5].user_id, self.user_sales_manager, 'Assigned lead should not be reassigned')
@mute_logger('odoo.models.unlink')
def test_merge_assign_keep_master_team(self):
""" Check existing opportunity keep its team and salesman when merged with a new lead """
sales_team_dupe = self.env['crm.team'].create({
'name': 'Sales Team Dupe',
'sequence': 15,
'alias_name': False,
'use_leads': True,
'use_opportunities': True,
'company_id': False,
'user_id': False,
'assignment_domain': "[]",
})
self.env['crm.team.member'].create({
'user_id': self.user_sales_salesman.id,
'crm_team_id': sales_team_dupe.id,
'assignment_max': 10,
'assignment_domain': "[]",
})
master_opp = self.env['crm.lead'].create({
'name': 'Master',
'type': 'opportunity',
'probability': 50,
'partner_id': self.contact_1.id,
'team_id': self.sales_team_1.id,
'user_id': self.user_sales_manager.id,
})
dupe_lead = self.env['crm.lead'].create({
'name': 'Dupe',
'type': 'lead',
'email_from': 'Duplicate Email <%s>' % master_opp.email_normalized,
'probability': 10,
'team_id': False,
'user_id': False,
})
sales_team_dupe._action_assign_leads(work_days=2)
self.assertFalse(dupe_lead.exists())
self.assertEqual(master_opp.team_id, self.sales_team_1, 'Opportunity: should keep its sales team')
self.assertEqual(master_opp.user_id, self.user_sales_manager, 'Opportunity: should keep its salesman')
def test_no_assign_if_exceed_max_assign(self):
""" Test no leads being assigned to any team member if weights list sums to 0"""
leads = self._create_leads_batch(
lead_type='lead',
user_ids=[False],
count=1
)
sales_team_4 = self.env['crm.team'].create({
'name': 'Sales Team 4',
'sequence': 15,
'use_leads': True,
})
sales_team_4_m1 = self.env['crm.team.member'].create({
'user_id': self.user_sales_salesman.id,
'crm_team_id': sales_team_4.id,
'assignment_max': 30,
})
sales_team_4_m1.lead_month_count = 50
leads.team_id = sales_team_4.id
members_data = sales_team_4_m1._assign_and_convert_leads(work_days=0.2)
self.assertEqual(
len(members_data[sales_team_4_m1]['assigned']),
0,
"If team member has lead count greater than max assign,then do not assign any more")

View file

@ -0,0 +1,668 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import SUPERUSER_ID
from odoo.addons.crm.tests import common as crm_common
from odoo.fields import Datetime
from odoo.tests.common import tagged, users
from odoo.tests.common import Form
@tagged('lead_manage')
class TestLeadConvertForm(crm_common.TestLeadConvertCommon):
@users('user_sales_manager')
def test_form_action_default(self):
""" Test Lead._find_matching_partner() """
lead = self.env['crm.lead'].browse(self.lead_1.ids)
customer = self.env['res.partner'].create({
"name": "Amy Wong",
"email": '"Amy, PhD Student, Wong" Tiny <AMY.WONG@test.example.com>'
})
wizard = Form(self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': lead.id,
'active_ids': lead.ids,
}))
self.assertEqual(wizard.name, 'convert')
self.assertEqual(wizard.action, 'exist')
self.assertEqual(wizard.partner_id, customer)
@users('user_sales_manager')
def test_form_name_onchange(self):
""" Test Lead._find_matching_partner() """
lead = self.env['crm.lead'].browse(self.lead_1.ids)
lead_dup = lead.copy({'name': 'Duplicate'})
customer = self.env['res.partner'].create({
"name": "Amy Wong",
"email": '"Amy, PhD Student, Wong" Tiny <AMY.WONG@test.example.com>'
})
wizard = Form(self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': lead.id,
'active_ids': lead.ids,
}))
self.assertEqual(wizard.name, 'merge')
self.assertEqual(wizard.action, 'exist')
self.assertEqual(wizard.partner_id, customer)
self.assertEqual(wizard.duplicated_lead_ids[:], lead + lead_dup)
wizard.name = 'convert'
wizard.action = 'create'
self.assertEqual(wizard.action, 'create', 'Should keep user input')
self.assertEqual(wizard.name, 'convert', 'Should keep user input')
@tagged('lead_manage')
class TestLeadConvert(crm_common.TestLeadConvertCommon):
"""
TODO: created partner (handle assignation) has team of lead
TODO: create partner has user_id coming from wizard
"""
@classmethod
def setUpClass(cls):
super(TestLeadConvert, cls).setUpClass()
date = Datetime.from_string('2020-01-20 16:00:00')
cls.crm_lead_dt_mock.now.return_value = date
@users('user_sales_manager')
def test_duplicates_computation(self):
""" Test Lead._get_lead_duplicates() and check won / probability usage """
test_lead = self.env['crm.lead'].browse(self.lead_1.ids)
customer, dup_leads = self._create_duplicates(test_lead)
dup_leads += self.env['crm.lead'].create([
{'name': 'Duplicate lead: same email_from, lost',
'type': 'lead',
'email_from': test_lead.email_from,
'probability': 0, 'active': False,
},
{'name': 'Duplicate lead: same email_from, proba 0 but not lost',
'type': 'lead',
'email_from': test_lead.email_from,
'probability': 0, 'active': True,
},
{'name': 'Duplicate opp: same email_from, won',
'type': 'opportunity',
'email_from': test_lead.email_from,
'probability': 100, 'stage_id': self.stage_team1_won.id,
},
{'name': 'Duplicate opp: same email_from, proba 100 but not won',
'type': 'opportunity',
'email_from': test_lead.email_from,
'probability': 100, 'stage_id': self.stage_team1_2.id,
}
])
lead_lost = dup_leads.filtered(lambda lead: lead.name == 'Duplicate lead: same email_from, lost')
_opp_proba100 = dup_leads.filtered(lambda lead: lead.name == 'Duplicate opp: same email_from, proba 100 but not won')
opp_won = dup_leads.filtered(lambda lead: lead.name == 'Duplicate opp: same email_from, won')
opp_lost = dup_leads.filtered(lambda lead: lead.name == 'Duplicate: lost opportunity')
test_lead.write({'partner_id': customer.id})
# not include_lost = remove archived leads as well as 'won' opportunities
result = test_lead._get_lead_duplicates(
partner=test_lead.partner_id,
email=test_lead.email_from,
include_lost=False
)
self.assertEqual(result, test_lead + dup_leads - (lead_lost + opp_won + opp_lost))
# include_lost = remove archived opp only
result = test_lead._get_lead_duplicates(
partner=test_lead.partner_id,
email=test_lead.email_from,
include_lost=True
)
self.assertEqual(result, test_lead + dup_leads - (lead_lost))
def test_initial_data(self):
""" Ensure initial data to avoid spaghetti test update afterwards """
self.assertFalse(self.lead_1.date_conversion)
self.assertEqual(self.lead_1.date_open, Datetime.from_string('2020-01-15 11:30:00'))
self.assertEqual(self.lead_1.lang_id, self.lang_fr)
self.assertFalse(self.lead_1.mobile)
self.assertEqual(self.lead_1.phone, '+1 202 555 9999')
self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
self.assertEqual(self.lead_1.team_id, self.sales_team_1)
self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
@users('user_sales_manager')
def test_lead_convert_base(self):
""" Test base method ``convert_opportunity`` or crm.lead model """
self.contact_2.phone = False # force Falsy to compare with mobile
self.assertFalse(self.contact_2.phone)
lead = self.lead_1.with_user(self.env.user)
lead.write({
'phone': '123456789',
})
self.assertEqual(lead.team_id, self.sales_team_1)
self.assertEqual(lead.stage_id, self.stage_team1_1)
self.assertEqual(lead.email_from, 'amy.wong@test.example.com')
self.assertEqual(lead.lang_id, self.lang_fr)
lead.convert_opportunity(self.contact_2)
self.assertEqual(lead.type, 'opportunity')
self.assertEqual(lead.partner_id, self.contact_2)
self.assertEqual(lead.email_from, self.contact_2.email)
self.assertEqual(lead.lang_id, self.lang_en)
self.assertEqual(lead.mobile, self.contact_2.mobile)
self.assertEqual(lead.phone, '123456789')
self.assertEqual(lead.team_id, self.sales_team_1)
self.assertEqual(lead.stage_id, self.stage_team1_1)
@users('user_sales_manager')
def test_lead_convert_base_corner_cases(self):
""" Test base method ``convert_opportunity`` or crm.lead model with corner
cases: inactive, won, stage update, ... """
# inactive leads are not converted
lead = self.lead_1.with_user(self.env.user)
lead.action_archive()
self.assertFalse(lead.active)
lead.convert_opportunity(self.contact_2)
self.assertEqual(lead.type, 'lead')
self.assertEqual(lead.partner_id, self.env['res.partner'])
lead.action_unarchive()
self.assertTrue(lead.active)
# won leads are not converted
lead.action_set_won()
# TDE FIXME: set won does not take into account sales team when fetching a won stage
# self.assertEqual(lead.stage_id, self.stage_team1_won)
self.assertEqual(lead.stage_id, self.stage_gen_won)
self.assertEqual(lead.probability, 100)
lead.convert_opportunity(self.contact_2)
self.assertEqual(lead.type, 'lead')
self.assertEqual(lead.partner_id, self.env['res.partner'])
@users('user_sales_manager')
def test_lead_convert_base_w_salesmen(self):
""" Test base method ``convert_opportunity`` while forcing salesmen, as it
should also force sales team """
lead = self.lead_1.with_user(self.env.user)
self.assertEqual(lead.team_id, self.sales_team_1)
lead.convert_opportunity(False, user_ids=self.user_sales_salesman.ids)
self.assertEqual(lead.user_id, self.user_sales_salesman)
self.assertEqual(lead.team_id, self.sales_team_convert)
# TDE FIXME: convert does not recompute stage based on updated team of assigned user
# self.assertEqual(lead.stage_id, self.stage_team_convert_1)
@users('user_sales_manager')
def test_lead_convert_base_w_team(self):
""" Test base method ``convert_opportunity`` while forcing team """
lead = self.lead_1.with_user(self.env.user)
lead.convert_opportunity(False, team_id=self.sales_team_convert.id)
self.assertEqual(lead.team_id, self.sales_team_convert)
self.assertEqual(lead.user_id, self.user_sales_leads)
# TDE FIXME: convert does not recompute stage based on team
# self.assertEqual(lead.stage_id, self.stage_team_convert_1)
@users('user_sales_manager')
def test_lead_convert_corner_cases_crud(self):
""" Test Lead._find_matching_partner() """
# email formatting
other_lead = self.lead_1.copy()
other_lead.write({'partner_id': self.contact_1.id})
convert = self.env['crm.lead2opportunity.partner'].with_context({
'default_lead_id': other_lead.id,
}).create({})
self.assertEqual(convert.lead_id, other_lead)
self.assertEqual(convert.partner_id, self.contact_1)
self.assertEqual(convert.action, 'exist')
convert = self.env['crm.lead2opportunity.partner'].with_context({
'default_lead_id': other_lead.id,
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
}).create({})
self.assertEqual(convert.lead_id, other_lead)
self.assertEqual(convert.partner_id, self.contact_1)
self.assertEqual(convert.action, 'exist')
@users('user_sales_manager')
def test_lead_convert_corner_cases_matching(self):
""" Test Lead._find_matching_partner() """
# email formatting
self.lead_1.write({
'email_from': 'Amy Wong <amy.wong@test.example.com>'
})
customer = self.env['res.partner'].create({
'name': 'Different Name',
'email': 'Wong AMY <AMY.WONG@test.example.com>'
})
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
'active_ids': self.lead_1.ids,
}).create({})
# TDE FIXME: should take into account normalized email version, not encoded one
# self.assertEqual(convert.partner_id, customer)
@users('user_sales_manager')
def test_lead_convert_no_lang(self):
""" Ensure converting a lead with an archived language correctly falls back on the default partner language. """
inactive_lang = self.env["res.lang"].sudo().create({
'code': 'en_ZZ',
'name': 'Inactive Lang',
'active': False,
})
lead = self.lead_1.with_user(self.env.user)
lead.lang_id = inactive_lang
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
'active_ids': self.lead_1.ids,
}).create({'action': 'create'})
convert.action_apply()
self.assertTrue(lead.partner_id)
self.assertEqual(lead.partner_id.lang, 'en_US')
@users('user_sales_manager')
def test_lead_convert_internals(self):
""" Test internals of convert wizard """
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
'active_ids': self.lead_1.ids,
}).create({})
# test internals of convert wizard
self.assertEqual(convert.lead_id, self.lead_1)
self.assertEqual(convert.user_id, self.lead_1.user_id)
self.assertEqual(convert.team_id, self.lead_1.team_id)
self.assertFalse(convert.partner_id)
self.assertEqual(convert.name, 'convert')
self.assertEqual(convert.action, 'create')
convert.write({'user_id': self.user_sales_salesman.id})
self.assertEqual(convert.user_id, self.user_sales_salesman)
self.assertEqual(convert.team_id, self.sales_team_convert)
convert.action_apply()
# convert test
self.assertEqual(self.lead_1.type, 'opportunity')
self.assertEqual(self.lead_1.user_id, self.user_sales_salesman)
self.assertEqual(self.lead_1.team_id, self.sales_team_convert)
# TDE FIXME: stage is linked to the old sales team and is not updated when converting, could be improved
# self.assertEqual(self.lead_1.stage_id, self.stage_gen_1)
# partner creation test
new_partner = self.lead_1.partner_id
self.assertEqual(new_partner.email, 'amy.wong@test.example.com')
self.assertEqual(new_partner.lang, self.lang_fr.code)
self.assertEqual(new_partner.phone, '+1 202 555 9999')
self.assertEqual(new_partner.name, 'Amy Wong')
@users('user_sales_manager')
def test_lead_convert_action_exist(self):
""" Test specific use case of 'exist' action in conver wizard """
self.lead_1.write({'partner_id': self.contact_1.id})
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
'active_ids': self.lead_1.ids,
}).create({})
self.assertEqual(convert.action, 'exist')
convert.action_apply()
self.assertEqual(self.lead_1.type, 'opportunity')
self.assertEqual(self.lead_1.partner_id, self.contact_1)
@users('user_sales_manager')
def test_lead_convert_action_nothing(self):
""" Test specific use case of 'nothing' action in conver wizard """
self.lead_1.write({'contact_name': False})
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
'active_ids': self.lead_1.ids,
}).create({})
self.assertEqual(convert.action, 'nothing')
convert.action_apply()
self.assertEqual(self.lead_1.type, 'opportunity')
self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
self.assertEqual(self.lead_1.team_id, self.sales_team_1)
self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
self.assertEqual(self.lead_1.partner_id, self.env['res.partner'])
@users('user_sales_manager')
def test_lead_convert_contact_mutlicompany(self):
""" Check the wizard convert to opp don't find contact
You are not able to see because they belong to another company """
# Use superuser_id because creating a company with a user add directly
# the company in company_ids of the user.
company_2 = self.env['res.company'].with_user(SUPERUSER_ID).create({'name': 'Company 2'})
partner_company_2 = self.env['res.partner'].with_user(SUPERUSER_ID).create({
'name': 'Contact in other company',
'email': 'test@company2.com',
'company_id': company_2.id,
})
lead = self.env['crm.lead'].create({
'name': 'LEAD',
'type': 'lead',
'email_from': 'test@company2.com',
})
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': lead.id,
'active_ids': lead.ids,
}).create({'name': 'convert', 'action': 'exist'})
self.assertNotEqual(convert.partner_id, partner_company_2,
"Conversion wizard should not be able to find the partner from another company")
@users('user_sales_manager')
def test_lead_convert_same_partner(self):
""" Check that we don't erase lead information
with existing partner info if the partner is already set
"""
partner = self.env['res.partner'].create({
'name': 'Empty partner',
})
lead = self.env['crm.lead'].create({
'name': 'LEAD',
'partner_id': partner.id,
'type': 'lead',
'email_from': 'demo@test.com',
'lang_id': self.lang_fr.id,
'street': 'my street',
'city': 'my city',
})
lead.convert_opportunity(partner)
self.assertEqual(lead.email_from, 'demo@test.com', 'Email From should be preserved during conversion')
self.assertEqual(lead.lang_id, self.lang_fr, 'Lang should be preserved during conversion')
self.assertEqual(lead.street, 'my street', 'Street should be preserved during conversion')
self.assertEqual(lead.city, 'my city', 'City should be preserved during conversion')
self.assertEqual(partner.lang, 'en_US')
@users('user_sales_manager')
def test_lead_convert_properties_preserve(self):
"""Verify that the properties are preserved when converting."""
initial_team = self.lead_1.with_env(self.env).team_id
self.lead_1.lead_properties = [{
'name': 'test',
'type': 'char',
'value': 'test value',
'definition_changed': True,
}]
self.lead_1.convert_opportunity(False)
self.assertEqual(self.lead_1.team_id, initial_team)
self.assertEqual(self.lead_1.lead_properties, [{
'name': 'test',
'type': 'char',
'value': 'test value',
}])
# re-writing the team, but keeping the same value should not reset the properties
self.lead_1.write({'team_id': self.lead_1.team_id.id})
self.assertEqual(self.lead_1.lead_properties, [{
'name': 'test',
'type': 'char',
'value': 'test value',
}])
@users('user_sales_manager')
def test_lead_convert_properties_reset(self):
"""Verify that the properties are reset when converting if the team changed."""
initial_team = self.lead_1.with_env(self.env).team_id
self.lead_1.lead_properties = [{
'name': 'test',
'type': 'char',
'value': 'test value',
'definition_changed': True,
}]
self.lead_1.convert_opportunity(False, user_ids=self.user_sales_salesman.ids)
self.assertNotEqual(self.lead_1.team_id, initial_team)
self.assertFalse(self.lead_1.lead_properties)
@users('user_sales_manager')
def test_lead_merge(self):
""" Test convert wizard working in merge mode """
date = Datetime.from_string('2020-01-20 16:00:00')
self.crm_lead_dt_mock.now.return_value = date
leads = self.env['crm.lead']
for x in range(2):
leads |= self.env['crm.lead'].create({
'name': 'Dup-%02d-%s' % (x+1, self.lead_1.name),
'type': 'lead', 'user_id': False, 'team_id': self.lead_1.team_id.id,
'contact_name': 'Duplicate %02d of %s' % (x+1, self.lead_1.contact_name),
'email_from': self.lead_1.email_from,
'probability': 10,
})
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
'active_ids': self.lead_1.ids,
}).create({})
# test internals of convert wizard
self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | leads)
self.assertEqual(convert.user_id, self.lead_1.user_id)
self.assertEqual(convert.team_id, self.lead_1.team_id)
self.assertFalse(convert.partner_id)
self.assertEqual(convert.name, 'merge')
self.assertEqual(convert.action, 'create')
convert.write({'user_id': self.user_sales_salesman.id})
self.assertEqual(convert.user_id, self.user_sales_salesman)
self.assertEqual(convert.team_id, self.sales_team_convert)
convert.action_apply()
self.assertEqual(self.lead_1.type, 'opportunity')
@users('user_sales_manager')
def test_lead_merge_last_created(self):
"""
Test convert wizard is not deleted in merge mode when the original assigned lead is deleted
"""
date = Datetime.from_string('2020-01-20 16:00:00')
self.crm_lead_dt_mock.now.return_value = date
last_lead = self.env['crm.lead'].create({
'name': f'Duplicate of {self.lead_1.contact_name}',
'type': 'lead', 'user_id': False, 'team_id': self.lead_1.team_id.id,
'contact_name': f'Duplicate of {self.lead_1.contact_name}',
'email_from': self.lead_1.email_from,
'probability': 10,
})
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': last_lead.id,
'active_ids': last_lead.ids,
}).create({})
# test main lead on wizard
self.assertEqual(convert.lead_id, last_lead)
convert.action_apply()
self.assertTrue(convert.exists(), 'Wizard cannot be deleted via cascade!')
self.assertEqual(convert.lead_id, self.lead_1, "Lead must be the result opportunity!")
self.assertEqual(self.lead_1.type, 'opportunity')
self.assertFalse(last_lead.exists(), 'The last lead must be merged with the first one!')
@users('user_sales_salesman')
def test_lead_merge_user(self):
""" Test convert wizard working in merge mode with sales user """
date = Datetime.from_string('2020-01-20 16:00:00')
self.crm_lead_dt_mock.now.return_value = date
leads = self.env['crm.lead']
for x in range(2):
leads |= self.env['crm.lead'].create({
'name': 'Dup-%02d-%s' % (x+1, self.lead_1.name),
'type': 'lead', 'user_id': False, 'team_id': self.lead_1.team_id.id,
'contact_name': 'Duplicate %02d of %s' % (x+1, self.lead_1.contact_name),
'email_from': self.lead_1.email_from,
'probability': 10,
})
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': leads[0].id,
'active_ids': leads[0].ids,
}).create({})
# test internals of convert wizard
self.assertEqual(convert.duplicated_lead_ids, leads)
self.assertEqual(convert.name, 'merge')
self.assertEqual(convert.action, 'create')
convert.write({'user_id': self.user_sales_salesman.id})
self.assertEqual(convert.user_id, self.user_sales_salesman)
self.assertEqual(convert.team_id, self.sales_team_convert)
convert.action_apply()
self.assertEqual(leads[0].type, 'opportunity')
@users('user_sales_manager')
def test_lead_merge_duplicates(self):
""" Test Lead._get_lead_duplicates() and check: partner / email fallbacks """
customer, dup_leads = self._create_duplicates(self.lead_1)
lead_partner = dup_leads.filtered(lambda lead: lead.name == 'Duplicate: customer ID')
self.assertTrue(bool(lead_partner))
self.lead_1.write({
'partner_id': customer.id,
})
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
'active_ids': self.lead_1.ids,
}).create({})
self.assertEqual(convert.partner_id, customer)
self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | dup_leads)
# Check: partner fallbacks
self.lead_1.write({
'email_from': False,
'partner_id': customer.id,
})
customer.write({'email': False})
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
'active_ids': self.lead_1.ids,
}).create({})
self.assertEqual(convert.partner_id, customer)
self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | lead_partner)
@users('user_sales_manager')
def test_lead_merge_duplicates_flow(self):
""" Test Lead._get_lead_duplicates() + merge with active_test """
# Check: email formatting
self.lead_1.write({
'email_from': 'Amy Wong <amy.wong@test.example.com>'
})
customer, dup_leads = self._create_duplicates(self.lead_1)
opp_lost = dup_leads.filtered(lambda lead: lead.name == 'Duplicate: lost opportunity')
self.assertTrue(bool(opp_lost))
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
'active_ids': self.lead_1.ids,
}).create({})
self.assertEqual(convert.partner_id, customer)
self.assertEqual(convert.duplicated_lead_ids, self.lead_1 | dup_leads)
convert.action_apply()
self.assertEqual(
(self.lead_1 | dup_leads).exists(),
opp_lost)
@tagged('lead_manage')
class TestLeadConvertBatch(crm_common.TestLeadConvertMassCommon):
def test_initial_data(self):
""" Ensure initial data to avoid spaghetti test update afterwards """
self.assertFalse(self.lead_1.date_conversion)
self.assertEqual(self.lead_1.date_open, Datetime.from_string('2020-01-15 11:30:00'))
self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
self.assertEqual(self.lead_1.team_id, self.sales_team_1)
self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
self.assertEqual(self.lead_w_partner.stage_id, self.env['crm.stage'])
self.assertEqual(self.lead_w_partner.user_id, self.user_sales_manager)
self.assertEqual(self.lead_w_partner.team_id, self.sales_team_1)
self.assertEqual(self.lead_w_partner_company.stage_id, self.stage_team1_1)
self.assertEqual(self.lead_w_partner_company.user_id, self.user_sales_manager)
self.assertEqual(self.lead_w_partner_company.team_id, self.sales_team_1)
self.assertEqual(self.lead_w_contact.stage_id, self.stage_gen_1)
self.assertEqual(self.lead_w_contact.user_id, self.user_sales_salesman)
self.assertEqual(self.lead_w_contact.team_id, self.sales_team_convert)
self.assertEqual(self.lead_w_email.stage_id, self.stage_gen_1)
self.assertEqual(self.lead_w_email.user_id, self.user_sales_salesman)
self.assertEqual(self.lead_w_email.team_id, self.sales_team_convert)
self.assertEqual(self.lead_w_email_lost.stage_id, self.stage_team1_2)
self.assertEqual(self.lead_w_email_lost.user_id, self.user_sales_leads)
self.assertEqual(self.lead_w_email_lost.team_id, self.sales_team_1)
@users('user_sales_manager')
def test_lead_convert_batch_internals(self):
""" Test internals of convert wizard, working in batch mode """
date = self.env.cr.now()
lead_w_partner = self.lead_w_partner
lead_w_contact = self.lead_w_contact
lead_w_email_lost = self.lead_w_email_lost
lead_w_email_lost.action_set_lost()
self.assertEqual(lead_w_email_lost.active, False)
convert = self.env['crm.lead2opportunity.partner'].with_context({
'active_model': 'crm.lead',
'active_id': self.lead_1.id,
'active_ids': (self.lead_1 | lead_w_partner | lead_w_contact | lead_w_email_lost).ids,
}).create({})
# test internals of convert wizard
# self.assertEqual(convert.lead_id, self.lead_1)
self.assertEqual(convert.user_id, self.lead_1.user_id)
self.assertEqual(convert.team_id, self.lead_1.team_id)
self.assertFalse(convert.partner_id)
self.assertEqual(convert.name, 'convert')
self.assertEqual(convert.action, 'create')
convert.action_apply()
self.assertEqual(convert.user_id, self.user_sales_leads)
self.assertEqual(convert.team_id, self.sales_team_1)
# lost leads are not converted (see crm_lead.convert_opportunity())
self.assertFalse(lead_w_email_lost.active)
self.assertFalse(lead_w_email_lost.date_conversion)
self.assertEqual(lead_w_email_lost.partner_id, self.env['res.partner'])
self.assertEqual(lead_w_email_lost.stage_id, self.stage_team1_2) # did not change
# other leads are converted into opportunities
for opp in (self.lead_1 | lead_w_partner | lead_w_contact):
# team management update: opportunity linked to chosen wizard values
self.assertEqual(opp.type, 'opportunity')
self.assertTrue(opp.active)
self.assertEqual(opp.user_id, convert.user_id)
self.assertEqual(opp.team_id, convert.team_id)
# dates update: convert set them to now
self.assertEqual(opp.date_open, date)
self.assertEqual(opp.date_conversion, date)
# stage update (depends on previous value)
if opp == self.lead_1:
self.assertEqual(opp.stage_id, self.stage_team1_1) # did not change
elif opp == lead_w_partner:
self.assertEqual(opp.stage_id, self.stage_team1_1) # is set to default stage of sales_team_1
elif opp == lead_w_contact:
self.assertEqual(opp.stage_id, self.stage_gen_1) # did not change
else:
self.assertFalse(True)

View file

@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests import common as crm_common
from odoo.tests.common import tagged, users
@tagged('lead_manage', 'crm_performance', 'post_install', '-at_install')
class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
@classmethod
def setUpClass(cls):
super(TestLeadConvertMass, cls).setUpClass()
cls.leads = cls.lead_1 + cls.lead_w_partner + cls.lead_w_email_lost
cls.assign_users = cls.user_sales_manager + cls.user_sales_leads_convert + cls.user_sales_salesman
@users('user_sales_manager')
def test_assignment_salesmen(self):
test_leads = self._create_leads_batch(count=50, user_ids=[False])
user_ids = self.assign_users.ids
self.assertEqual(test_leads.user_id, self.env['res.users'])
with self.assertQueryCount(user_sales_manager=0):
test_leads = self.env['crm.lead'].browse(test_leads.ids)
with self.assertQueryCount(user_sales_manager=543): # crm 537 / com 543 / ent 537
test_leads._handle_salesmen_assignment(user_ids=user_ids, team_id=False)
self.assertEqual(test_leads.team_id, self.sales_team_convert | self.sales_team_1)
self.assertEqual(test_leads[0::3].user_id, self.user_sales_manager)
self.assertEqual(test_leads[1::3].user_id, self.user_sales_leads_convert)
self.assertEqual(test_leads[2::3].user_id, self.user_sales_salesman)
@users('user_sales_manager')
def test_assignment_salesmen_wteam(self):
test_leads = self._create_leads_batch(count=50, user_ids=[False])
user_ids = self.assign_users.ids
team_id = self.sales_team_convert.id
self.assertEqual(test_leads.user_id, self.env['res.users'])
with self.assertQueryCount(user_sales_manager=0):
test_leads = self.env['crm.lead'].browse(test_leads.ids)
with self.assertQueryCount(user_sales_manager=524): # crm 521 / com 524
test_leads._handle_salesmen_assignment(user_ids=user_ids, team_id=team_id)
self.assertEqual(test_leads.team_id, self.sales_team_convert)
self.assertEqual(test_leads[0::3].user_id, self.user_sales_manager)
self.assertEqual(test_leads[1::3].user_id, self.user_sales_leads_convert)
self.assertEqual(test_leads[2::3].user_id, self.user_sales_salesman)
@users('user_sales_manager')
def test_mass_convert_internals(self):
""" Test internals mass converted in convert mode, without duplicate management """
# reset some assigned users to test salesmen assign
(self.lead_w_partner | self.lead_w_email_lost).write({
'user_id': False
})
mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
'active_model': 'crm.lead',
'active_ids': self.leads.ids,
'active_id': self.leads.ids[0]
}).create({
'deduplicate': False,
'user_id': self.user_sales_salesman.id,
'force_assignment': False,
})
# default values
self.assertEqual(mass_convert.name, 'convert')
self.assertEqual(mass_convert.action, 'each_exist_or_create')
# depending on options
self.assertEqual(mass_convert.partner_id, self.env['res.partner'])
self.assertEqual(mass_convert.deduplicate, False)
self.assertEqual(mass_convert.user_id, self.user_sales_salesman)
self.assertEqual(mass_convert.team_id, self.sales_team_convert)
mass_convert.action_mass_convert()
for lead in self.lead_1 | self.lead_w_partner:
self.assertEqual(lead.type, 'opportunity')
if lead == self.lead_w_partner:
self.assertEqual(lead.user_id, self.env['res.users']) # user_id is bypassed
self.assertEqual(lead.partner_id, self.contact_1)
elif lead == self.lead_1:
self.assertEqual(lead.user_id, self.user_sales_leads) # existing value not forced
new_partner = lead.partner_id
self.assertEqual(new_partner.name, 'Amy Wong')
self.assertEqual(new_partner.email, 'amy.wong@test.example.com')
# test unforced assignation
mass_convert.write({
'user_ids': self.user_sales_salesman.ids,
})
mass_convert.action_mass_convert()
self.assertEqual(self.lead_w_partner.user_id, self.user_sales_salesman)
self.assertEqual(self.lead_1.user_id, self.user_sales_leads) # existing value not forced
# lost leads are untouched
self.assertEqual(self.lead_w_email_lost.type, 'lead')
self.assertFalse(self.lead_w_email_lost.active)
self.assertFalse(self.lead_w_email_lost.date_conversion)
# TDE FIXME: partner creation is done even on lost leads because not checked in wizard
# self.assertEqual(self.lead_w_email_lost.partner_id, self.env['res.partner'])
@users('user_sales_manager')
def test_mass_convert_deduplicate(self):
""" Test duplicated_lead_ids fields having another behavior in mass convert
because why not. Its use is: among leads under convert, store those with
duplicates if deduplicate is set to True. """
_customer, lead_1_dups = self._create_duplicates(self.lead_1, create_opp=False)
lead_1_final = self.lead_1 # after merge: same but with lower ID
_customer2, lead_w_partner_dups = self._create_duplicates(self.lead_w_partner, create_opp=False)
lead_w_partner_final = lead_w_partner_dups[0] # lead_w_partner has no stage -> lower in sort by confidence
lead_w_partner_dups_partner = lead_w_partner_dups[1] # copy with a partner_id (with the same email)
mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
'active_model': 'crm.lead',
'active_ids': self.leads.ids,
}).create({
'deduplicate': True,
})
self.assertEqual(mass_convert.action, 'each_exist_or_create')
self.assertEqual(mass_convert.name, 'convert')
self.assertEqual(mass_convert.lead_tomerge_ids, self.leads)
self.assertEqual(mass_convert.duplicated_lead_ids, self.lead_1 | self.lead_w_partner)
mass_convert.action_mass_convert()
self.assertEqual(
(lead_1_dups | lead_w_partner_dups | lead_w_partner_dups_partner).exists(),
lead_w_partner_final
)
for lead in lead_1_final | lead_w_partner_final:
self.assertTrue(lead.active)
self.assertEqual(lead.type, 'opportunity')
@users('user_sales_manager')
def test_mass_convert_find_existing(self):
""" Check that we don't find a wrong partner
that have similar name during mass conversion
"""
wrong_partner = self.env['res.partner'].create({
'name': 'casa depapel',
'street': "wrong street"
})
lead = self.env['crm.lead'].create({'name': 'Asa Depape'})
mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
'active_model': 'crm.lead',
'active_ids': lead.ids,
'active_id': lead.ids[0]
}).create({
'deduplicate': False,
'action': 'each_exist_or_create',
'name': 'convert',
})
mass_convert.action_mass_convert()
self.assertNotEqual(lead.partner_id, wrong_partner, "Partner Id should not match the wrong contact")
@users('user_sales_manager')
def test_mass_convert_performances(self):
test_leads = self._create_leads_batch(count=50, user_ids=[False])
user_ids = self.assign_users.ids
# randomness: at least 1 query
with self.assertQueryCount(user_sales_manager=1704): # crm 1410 / com 1697
mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
'active_model': 'crm.lead',
'active_ids': test_leads.ids,
}).create({
'deduplicate': True,
'user_ids': user_ids,
'force_assignment': True,
})
mass_convert.action_mass_convert()
self.assertEqual(set(test_leads.mapped('type')), set(['opportunity']))
self.assertEqual(len(test_leads.partner_id), len(test_leads))
# TDE FIXME: strange
# self.assertEqual(test_leads.team_id, self.sales_team_convert | self.sales_team_1)
self.assertEqual(test_leads.team_id, self.sales_team_1)
self.assertEqual(test_leads[0::3].user_id, self.user_sales_manager)
self.assertEqual(test_leads[1::3].user_id, self.user_sales_leads_convert)
self.assertEqual(test_leads[2::3].user_id, self.user_sales_salesman)
@users('user_sales_manager')
def test_mass_convert_w_salesmen(self):
# reset some assigned users to test salesmen assign
(self.lead_w_partner | self.lead_w_email_lost).write({
'user_id': False
})
mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
'active_model': 'crm.lead',
'active_ids': self.leads.ids,
'active_id': self.leads.ids[0]
}).create({
'deduplicate': False,
'user_ids': self.assign_users.ids,
'force_assignment': True,
})
# TDE FIXME: what happens if we mix people from different sales team ? currently nothing, to check
mass_convert.action_mass_convert()
for idx, lead in enumerate(self.leads - self.lead_w_email_lost):
self.assertEqual(lead.type, 'opportunity')
assigned_user = self.assign_users[idx % len(self.assign_users)]
self.assertEqual(lead.user_id, assigned_user)

View file

@ -0,0 +1,228 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.tests.common import tagged, users
@tagged('lead_manage')
class TestLeadConvert(TestCrmCommon):
@users('user_sales_manager')
def test_potential_duplicates(self):
company = self.env['res.partner'].create({
'name': 'My company',
'email': 'mycompany@company.com',
'is_company': True,
'street': '57th Street',
'city': 'New New York',
'country_id': self.env.ref('base.us').id,
'zip': '12345',
})
partner_1 = self.env['res.partner'].create({
'name': 'Dave',
'email': 'dave@odoo.com',
'mobile': '+1 202 555 0123',
'phone': False,
'parent_id': company.id,
'is_company': False,
'street': 'Pearl street',
'city': 'California',
'country_id': self.env.ref('base.us').id,
'zip': '95826',
})
partner_2 = self.env['res.partner'].create({
'name': 'Eve',
'email': 'eve@odoo.com',
'mobile': '+1 202 555 3210',
'phone': False,
'parent_id': company.id,
'is_company': False,
'street': 'Wall street',
'city': 'New York',
'country_id': self.env.ref('base.us').id,
'zip': '54321',
})
lead_1 = self.env['crm.lead'].create({
'name': 'Lead 1',
'type': 'lead',
'partner_name': 'Alice',
'email_from': 'alice@odoo.com',
})
lead_2 = self.env['crm.lead'].create({
'name': 'Opportunity 1',
'type': 'opportunity',
'email_from': 'alice@odoo.com',
})
lead_3 = self.env['crm.lead'].create({
'name': 'Opportunity 2',
'type': 'opportunity',
'email_from': 'alice@odoo.com',
})
lead_4 = self.env['crm.lead'].create({
'name': 'Lead 2',
'type': 'lead',
'partner_name': 'Alice Doe'
})
lead_5 = self.env['crm.lead'].create({
'name': 'Opportunity 3',
'type': 'opportunity',
'partner_name': 'Alice Doe'
})
lead_6 = self.env['crm.lead'].create({
'name': 'Opportunity 4',
'type': 'opportunity',
'partner_name': 'Bob Doe'
})
lead_7 = self.env['crm.lead'].create({
'name': 'Opportunity 5',
'type': 'opportunity',
'partner_name': 'Bob Doe',
'email_from': 'bob@odoo.com',
})
lead_8 = self.env['crm.lead'].create({
'name': 'Opportunity 6',
'type': 'opportunity',
'email_from': 'bob@mymail.com',
})
lead_9 = self.env['crm.lead'].create({
'name': 'Opportunity 7',
'type': 'opportunity',
'email_from': 'alice@mymail.com',
})
lead_10 = self.env['crm.lead'].create({
'name': 'Opportunity 8',
'type': 'opportunity',
'probability': 0,
'active': False,
'email_from': 'alice@mymail.com',
})
lead_11 = self.env['crm.lead'].create({
'name': 'Opportunity 9',
'type': 'opportunity',
'contact_name': 'charlie'
})
lead_12 = self.env['crm.lead'].create({
'name': 'Opportunity 10',
'type': 'opportunity',
'contact_name': 'Charlie Chapelin',
})
lead_13 = self.env['crm.lead'].create({
'name': 'Opportunity 8',
'type': 'opportunity',
'partner_id': partner_1.id
})
lead_14 = self.env['crm.lead'].create({
'name': 'Lead 3',
'type': 'lead',
'partner_id': partner_2.id
})
self.assertEqual(lead_1 + lead_2 + lead_3, lead_1.duplicate_lead_ids)
self.assertEqual(lead_1 + lead_2 + lead_3, lead_2.duplicate_lead_ids)
self.assertEqual(lead_1 + lead_2 + lead_3, lead_3.duplicate_lead_ids)
self.assertEqual(lead_4 + lead_5, lead_4.duplicate_lead_ids)
self.assertEqual(lead_4 + lead_5, lead_5.duplicate_lead_ids)
self.assertEqual(lead_6 + lead_7, lead_6.duplicate_lead_ids)
self.assertEqual(lead_6 + lead_7, lead_7.duplicate_lead_ids)
self.assertEqual(lead_8 + lead_9 + lead_10, lead_8.duplicate_lead_ids)
self.assertEqual(lead_8 + lead_9 + lead_10, lead_9.duplicate_lead_ids)
self.assertEqual(lead_8 + lead_9 + lead_10, lead_10.duplicate_lead_ids)
self.assertEqual(lead_11 + lead_12, lead_11.duplicate_lead_ids)
self.assertEqual(lead_12, lead_12.duplicate_lead_ids)
self.assertEqual(lead_13 + lead_14, lead_13.duplicate_lead_ids)
self.assertEqual(lead_13 + lead_14, lead_14.duplicate_lead_ids)
@users('user_sales_manager')
def test_potential_duplicates_with_phone(self):
customer = self.env['res.partner'].create({
'email': 'customer1@duplicate.example.com',
'mobile': '+32485001122',
'name': 'Customer1',
'phone': '(803)-456-6126',
})
base_lead = self.env['crm.lead'].create({
'name': 'Base Lead',
'partner_id': customer.id,
'type': 'lead',
})
self.assertEqual(base_lead.contact_name, customer.name)
self.assertEqual(base_lead.mobile, customer.mobile)
self.assertFalse(base_lead.partner_name)
self.assertEqual(base_lead.phone, customer.phone)
dup1_1 = self.env['crm.lead'].create({
'name': 'Base Lead Dup1',
'type': 'lead',
'phone': '456-6126', # shorter version of base_lead
'mobile': ' ', # empty string shouldn't crash Odoo
'partner_name': 'Partner Name 1',
})
dup1_2 = self.env['crm.lead'].create({
'name': 'Base Lead Dup2',
'mobile': '8034566126',
'partner_name': 'Partner Name 2',
'type': 'lead',
})
dup1_3 = self.env['crm.lead'].create({
'name': 'Base Lead Dup3',
'partner_name': 'Partner Name 3',
'phone': '(803)-456-6126',
'type': 'lead',
})
dup1_4 = self.env['crm.lead'].create({
'mobile': '0032485001122',
# 'mobile': '0485001122', # note: does not work
'name': 'Base Lead Dup4',
'partner_name': 'Partner Name 4',
'phone': False,
'type': 'lead',
})
expected = base_lead + dup1_2 + dup1_3 + dup1_4 # dup1_1 is shorter than lead -> not a dupe
self.assertEqual(
base_lead.duplicate_lead_ids, expected,
'CRM: missing %s, extra %s' % ((expected - base_lead.duplicate_lead_ids).mapped('name'), (base_lead.duplicate_lead_ids - expected).mapped('name'))
)
expected = base_lead + dup1_1 + dup1_2 + dup1_3 # dup1_4 has mobile of customer, but no link with dup1_1
self.assertEqual(
dup1_1.duplicate_lead_ids, expected,
'CRM: missing %s, extra %s' % ((expected - dup1_1.duplicate_lead_ids).mapped('name'), (dup1_1.duplicate_lead_ids - expected).mapped('name'))
)
@users('user_sales_manager')
def test_potential_duplicates_with_invalid_email(self):
lead_1 = self.env['crm.lead'].create({
'name': 'Lead 1',
'type': 'lead',
'email_from': 'mail"1@mymail.com'
})
lead_2 = self.env['crm.lead'].create({
'name': 'Opportunity 1',
'type': 'opportunity',
'email_from': 'mail2@mymail.com'
})
lead_3 = self.env['crm.lead'].create({
'name': 'Opportunity 2',
'type': 'lead',
'email_from': 'odoo.com'
})
lead_4 = self.env['crm.lead'].create({
'name': 'Opportunity 3',
'type': 'opportunity',
'email_from': 'odoo.com'
})
lead_5 = self.env['crm.lead'].create({
'name': 'Opportunity 3',
'type': 'opportunity',
'email_from': 'myodoo.com'
})
self.assertEqual(lead_1 + lead_2, lead_1.duplicate_lead_ids)
self.assertEqual(lead_1 + lead_2, lead_2.duplicate_lead_ids)
self.assertEqual(lead_3 + lead_4 + lead_5, lead_3.duplicate_lead_ids)
self.assertEqual(lead_3 + lead_4 + lead_5, lead_4.duplicate_lead_ids)
self.assertEqual(lead_5, lead_5.duplicate_lead_ids)

View file

@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests import common as crm_common
from odoo.exceptions import AccessError
from odoo.tests.common import tagged, users
from odoo.tools import mute_logger
@tagged('lead_manage', 'lead_lost')
class TestLeadConvert(crm_common.TestCrmCommon):
@classmethod
def setUpClass(cls):
super(TestLeadConvert, cls).setUpClass()
cls.lost_reason = cls.env['crm.lost.reason'].create({
'name': 'Test Reason'
})
@users('user_sales_salesman')
def test_lead_lost(self):
""" Test setting a lead as lost using the wizard. Also check that an
'html editor' void content used as feedback is not logged on the lead. """
# Initial data
self.assertEqual(len(self.lead_1.message_ids), 1, 'Should contain creation message')
creation_message = self.lead_1.message_ids[0]
self.assertEqual(creation_message.subtype_id, self.env.ref('crm.mt_lead_create'))
# Update responsible as ACLs is "own only" for user_sales_salesman
self.lead_1.with_user(self.user_sales_manager).write({
'user_id': self.user_sales_salesman.id,
'probability': 32,
})
self.flush_tracking()
lead = self.env['crm.lead'].browse(self.lead_1.ids)
self.assertFalse(lead.lost_reason_id)
self.assertEqual(lead.probability, 32)
self.assertEqual(len(lead.message_ids), 2, 'Should have tracked new responsible')
update_message = lead.message_ids[0]
self.assertEqual(update_message.subtype_id, self.env.ref('mail.mt_note'))
# mark as lost using the wizard
lost_wizard = self.env['crm.lead.lost'].with_context({
'active_ids': lead.ids,
}).create({
'lost_reason_id': self.lost_reason.id,
'lost_feedback': '<p></p>', # void content
})
lost_wizard.action_lost_reason_apply()
self.flush_tracking()
# check lead update
self.assertFalse(lead.active)
self.assertEqual(lead.automated_probability, 0)
self.assertEqual(lead.lost_reason_id, self.lost_reason) # TDE FIXME: should be called lost_reason_id non didjou
self.assertEqual(lead.probability, 0)
# check messages
self.assertEqual(len(lead.message_ids), 3, 'Should have logged a tracking message for lost lead with reason')
update_message = lead.message_ids[0]
self.assertEqual(update_message.subtype_id, self.env.ref('crm.mt_lead_lost'))
self.assertEqual(len(update_message.tracking_value_ids), 2, 'Tracking: active, lost reason')
self.assertTracking(
update_message,
[('active', 'boolean', True, False),
('lost_reason_id', 'many2one', False, self.lost_reason)
]
)
@users('user_sales_leads')
def test_lead_lost_batch_wfeedback(self):
""" Test setting leads as lost in batch using the wizard, including a log
message. """
leads = self._create_leads_batch(lead_type='lead', count=10, probabilities=[10, 20, 30])
self.assertEqual(len(leads), 10)
self.flush_tracking()
lost_wizard = self.env['crm.lead.lost'].with_context({
'active_ids': leads.ids,
}).create({
'lost_reason_id': self.lost_reason.id,
'lost_feedback': '<p>I cannot find it. It was in my closet and pouf, disappeared.</p>',
})
lost_wizard.action_lost_reason_apply()
self.flush_tracking()
for lead in leads:
# check content
self.assertFalse(lead.active)
self.assertEqual(lead.automated_probability, 0)
self.assertEqual(lead.probability, 0)
self.assertEqual(lead.lost_reason_id, self.lost_reason)
# check messages
self.assertEqual(len(lead.message_ids), 2, 'Should have 2 messages: creation, lost with log')
lost_message = lead.message_ids.filtered(lambda msg: msg.subtype_id == self.env.ref('crm.mt_lead_lost'))
self.assertTrue(lost_message)
self.assertTracking(
lost_message,
[('active', 'boolean', True, False),
('lost_reason_id', 'many2one', False, self.lost_reason)
]
)
self.assertIn('<p>I cannot find it. It was in my closet and pouf, disappeared.</p>', lost_message.body,
'Feedback should be included directly within tracking message')
@users('user_sales_salesman')
@mute_logger('odoo.addons.base.models')
def test_lead_lost_crm_rights(self):
""" Test ACLs of lost reasons management and usage """
lead = self.lead_1.with_user(self.env.user)
# nice try little salesman but only managers can create lost reason to avoid bloating the DB
with self.assertRaises(AccessError):
lost_reason = self.env['crm.lost.reason'].create({
'name': 'Test Reason'
})
with self.with_user('user_sales_manager'):
lost_reason = self.env['crm.lost.reason'].create({
'name': 'Test Reason'
})
lost_wizard = self.env['crm.lead.lost'].with_context({
'active_ids': lead.ids
}).create({
'lost_reason_id': lost_reason.id
})
# nice try little salesman, you cannot invoke a wizard to update other people leads
with self.assertRaises(AccessError):
lost_wizard.action_lost_reason_apply()

View file

@ -0,0 +1,574 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from datetime import timedelta
from odoo.addons.crm.tests.common import TestLeadConvertMassCommon
from odoo.fields import Datetime
from odoo.tests.common import tagged, users
from odoo.tools import mute_logger
class TestLeadMergeCommon(TestLeadConvertMassCommon):
""" During a mixed merge (involving leads and opps), data should be handled a certain way following their type
(m2o, m2m, text, ...). """
@classmethod
def setUpClass(cls):
super(TestLeadMergeCommon, cls).setUpClass()
cls.leads = cls.lead_1 + cls.lead_w_partner + cls.lead_w_contact + cls.lead_w_email + cls.lead_w_partner_company + cls.lead_w_email_lost
# reset some assigned users to test salesmen assign
(cls.lead_w_partner | cls.lead_w_email_lost).write({
'user_id': False,
})
cls.lead_w_partner.write({'stage_id': False})
cls.lead_w_contact.write({'description': 'lead_w_contact'})
cls.lead_w_email.write({'description': 'lead_w_email'})
cls.lead_1.write({'description': 'lead_1'})
cls.lead_w_partner.write({'description': 'lead_w_partner'})
cls.assign_users = cls.user_sales_manager + cls.user_sales_leads_convert + cls.user_sales_salesman
@tagged('lead_manage')
class TestLeadMerge(TestLeadMergeCommon):
def _run_merge_wizard(self, leads):
res = self.env['crm.merge.opportunity'].with_context({
'active_model': 'crm.lead',
'active_ids': leads.ids,
'active_id': False,
}).create({
'team_id': False,
'user_id': False,
}).action_merge()
return self.env['crm.lead'].browse(res['res_id'])
def test_initial_data(self):
""" Ensure initial data to avoid spaghetti test update afterwards
Original order:
lead_w_contact ----------lead---seq=3----proba=15
lead_w_email ------------lead---seq=3----proba=15
lead_1 ------------------lead---seq=1----proba=?
lead_w_partner ----------lead---seq=False---proba=10
lead_w_partner_company --lead---seq=False---proba=15
"""
self.assertFalse(self.lead_1.date_conversion)
self.assertEqual(self.lead_1.date_open, Datetime.from_string('2020-01-15 11:30:00'))
self.assertEqual(self.lead_1.user_id, self.user_sales_leads)
self.assertEqual(self.lead_1.team_id, self.sales_team_1)
self.assertEqual(self.lead_1.stage_id, self.stage_team1_1)
self.assertEqual(self.lead_w_partner.stage_id, self.env['crm.stage'])
self.assertEqual(self.lead_w_partner.user_id, self.env['res.users'])
self.assertEqual(self.lead_w_partner.team_id, self.sales_team_1)
self.assertEqual(self.lead_w_partner_company.stage_id, self.stage_team1_1)
self.assertEqual(self.lead_w_partner_company.user_id, self.user_sales_manager)
self.assertEqual(self.lead_w_partner_company.team_id, self.sales_team_1)
self.assertEqual(self.lead_w_contact.stage_id, self.stage_gen_1)
self.assertEqual(self.lead_w_contact.user_id, self.user_sales_salesman)
self.assertEqual(self.lead_w_contact.team_id, self.sales_team_convert)
self.assertEqual(self.lead_w_email.stage_id, self.stage_gen_1)
self.assertEqual(self.lead_w_email.user_id, self.user_sales_salesman)
self.assertEqual(self.lead_w_email.team_id, self.sales_team_convert)
self.assertEqual(self.lead_w_email_lost.stage_id, self.stage_team1_2)
self.assertEqual(self.lead_w_email_lost.user_id, self.env['res.users'])
self.assertEqual(self.lead_w_email_lost.team_id, self.sales_team_1)
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_lead_merge_address_not_propagated(self):
"""All addresses have the same number of non-empty address fields, take the first one (lead_w_contact)
because it's the lead that has the best confidence level after being sorted with '_sort_by_confidence_level'"""
initial_address = {
'street': 'Test street',
'street2': 'Test street2',
'city': 'Test City',
'zip': '5000',
'state_id': False,
'country_id': self.env.ref('base.be'),
}
self.lead_w_contact.write(initial_address)
(self.leads - self.lead_w_contact).write({
'street': 'Other street',
'street2': 'Other street2',
'city': 'Other City',
'zip': '6666',
'state_id': self.env.ref('base.state_us_1'),
'country_id': False,
})
leads = self.env['crm.lead'].browse(self.leads.ids)._sort_by_confidence_level(reverse=True)
with self.assertLeadMerged(self.lead_w_contact, leads, **initial_address):
leads._merge_opportunity(auto_unlink=False, max_length=None)
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_lead_merge_address_propagated(self):
"""Test that the address with the most non-empty fields is propagated.
Should take the address of "lead_w_partner" (maximum number of non-empty address
fields and with an highest rank than "lead_w_email_lost")
"""
self.leads.write({
'street': 'Original street',
'street2': False,
'city': False,
'zip': False,
'state_id': False,
'country_id': False,
})
new_address = {
'street': 'New street',
'street2': False,
'city': 'New City',
'zip': False,
'state_id': False,
'country_id': False,
}
self.lead_w_partner.write(new_address)
self.lead_w_email_lost.write({
'street': 'Other street',
'street2': False,
'city': 'Other City',
'zip': False,
'state_id': False,
'country_id': False,
})
leads = self.env['crm.lead'].browse(self.leads.ids)._sort_by_confidence_level(reverse=True)
with self.assertLeadMerged(self.lead_w_contact, leads, **new_address):
leads._merge_opportunity(auto_unlink=False, max_length=None)
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_lead_merge_internals(self):
""" Test internals of merge wizard. In this test leads are ordered as
lead_w_contact --lead---seq=3---probability=25
lead_w_email ----lead---seq=3---probability=15
lead_1 ----------lead---seq=1
lead_w_partner --lead---seq=False
"""
# ensure initial data
self.lead_w_partner_company.action_set_won() # won opps should be excluded
merge = self.env['crm.merge.opportunity'].with_context({
'active_model': 'crm.lead',
'active_ids': self.leads.ids,
'active_id': False,
}).create({
'user_id': self.user_sales_leads_convert.id,
})
self.assertEqual(merge.team_id, self.sales_team_convert)
# TDE FIXME: not sure the browse in default get of wizard intended to exlude lost, as it browse ids
# and exclude inactive leads, but that's not written anywhere ... intended ??
self.assertEqual(merge.opportunity_ids, self.leads - self.lead_w_partner_company - self.lead_w_email_lost)
ordered_merge = self.lead_w_contact + self.lead_w_email + self.lead_1 + self.lead_w_partner
ordered_merge_description = '<br><br>'.join(l.description for l in ordered_merge)
# merged opportunity: in this test, all input are leads. Confidence is based on stage
# sequence -> lead_w_contact has a stage sequence of 3 and probability is greater
result = merge.action_merge()
merge_opportunity = self.env['crm.lead'].browse(result['res_id'])
self.assertFalse((ordered_merge - merge_opportunity).exists())
self.assertEqual(merge_opportunity, self.lead_w_contact)
self.assertEqual(merge_opportunity.type, 'lead')
self.assertEqual(merge_opportunity.description, ordered_merge_description)
# merged opportunity has updated salesman / team / stage is ok as generic
self.assertEqual(merge_opportunity.user_id, self.user_sales_leads_convert)
self.assertEqual(merge_opportunity.team_id, self.sales_team_convert)
self.assertEqual(merge_opportunity.stage_id, self.stage_gen_1)
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_lead_merge_mixed(self):
""" In case of mix, opportunities are on top, and result is an opportunity
lead_1 -------------------opp----seq=1---probability=60
lead_w_partner_company ---opp----seq=1---probability=50
lead_w_contact -----------lead---seq=3---probability=25
lead_w_email -------------lead---seq=3---probability=15
lead_w_partner -----------lead---seq=False
"""
# ensure initial data
(self.lead_w_partner_company | self.lead_1).write({'type': 'opportunity'})
self.lead_1.write({'probability': 60})
self.assertEqual(self.lead_w_partner_company.stage_id.sequence, 1)
self.assertEqual(self.lead_1.stage_id.sequence, 1)
merge = self.env['crm.merge.opportunity'].with_context({
'active_model': 'crm.lead',
'active_ids': self.leads.ids,
'active_id': False,
}).create({
'team_id': self.sales_team_convert.id,
'user_id': False,
})
# TDE FIXME: see aa44700dccdc2618e0b8bc94252789264104047c -> no user, no team -> strange
merge.write({'team_id': self.sales_team_convert.id})
# TDE FIXME: not sure the browse in default get of wizard intended to exlude lost, as it browse ids
# and exclude inactive leads, but that's not written anywhere ... intended ??
self.assertEqual(merge.opportunity_ids, self.leads - self.lead_w_email_lost)
ordered_merge = self.lead_w_partner_company + self.lead_w_contact + self.lead_w_email + self.lead_w_partner
result = merge.action_merge()
merge_opportunity = self.env['crm.lead'].browse(result['res_id'])
self.assertFalse((ordered_merge - merge_opportunity).exists())
self.assertEqual(merge_opportunity, self.lead_1)
self.assertEqual(merge_opportunity.type, 'opportunity')
# merged opportunity has same salesman (not updated in wizard)
self.assertEqual(merge_opportunity.user_id, self.user_sales_leads)
# TDE FIXME: as same uer_id is enforced, team is updated through onchange and therefore stage
self.assertEqual(merge_opportunity.team_id, self.sales_team_convert)
# self.assertEqual(merge_opportunity.team_id, self.sales_team_1)
# TDE FIXME: BUT team_id is computed after checking stage, based on wizard's team_id
self.assertEqual(merge_opportunity.stage_id, self.stage_team_convert_1)
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_lead_merge_probability_auto(self):
""" Check master lead keeps its automated probability when merged. """
self.lead_1.write({'type': 'opportunity', 'probability': self.lead_1.automated_probability})
self.assertTrue(self.lead_1.is_automated_probability)
leads = self.env['crm.lead'].browse((self.lead_1 + self.lead_w_partner + self.lead_w_partner_company).ids)
merged_lead = self._run_merge_wizard(leads)
self.assertEqual(merged_lead, self.lead_1)
self.assertTrue(merged_lead.is_automated_probability, "lead with Auto proba should remain with auto probability")
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_lead_merge_probability_auto_empty(self):
""" Check master lead keeps its automated probability when merged
even if its probability is 0. """
self.lead_1.write({'type': 'opportunity', 'probability': 0, 'automated_probability': 0})
self.assertTrue(self.lead_1.is_automated_probability)
leads = self.env['crm.lead'].browse((self.lead_1 + self.lead_w_partner + self.lead_w_partner_company).ids)
merged_lead = self._run_merge_wizard(leads)
self.assertEqual(merged_lead, self.lead_1)
self.assertTrue(merged_lead.is_automated_probability, "lead with Auto proba should remain with auto probability")
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_lead_merge_probability_manual(self):
""" Check master lead keeps its manual probability when merged. """
self.lead_1.write({'probability': 60})
self.assertFalse(self.lead_1.is_automated_probability)
leads = self.env['crm.lead'].browse((self.lead_1 + self.lead_w_partner + self.lead_w_partner_company).ids)
merged_lead = self._run_merge_wizard(leads)
self.assertEqual(merged_lead, self.lead_1)
self.assertEqual(merged_lead.probability, 60, "Manual Probability should remain the same after the merge")
self.assertFalse(merged_lead.is_automated_probability)
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_lead_merge_probability_manual_empty(self):
""" Check master lead keeps its manual probability when merged even if
its probability is 0. """
self.lead_1.write({'type': 'opportunity', 'probability': 0})
leads = self.env['crm.lead'].browse((self.lead_1 + self.lead_w_partner + self.lead_w_partner_company).ids)
merged_lead = self._run_merge_wizard(leads)
self.assertEqual(merged_lead, self.lead_1)
self.assertEqual(merged_lead.probability, 0, "Manual Probability should remain the same after the merge")
self.assertFalse(merged_lead.is_automated_probability)
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_merge_method(self):
""" In case of mix, opportunities are on top, and result is an opportunity
lead_1 -------------------opp----seq=1---probability=50
lead_w_partner_company ---opp----seq=1---probability=50 (ID greater)
lead_w_contact -----------lead---seq=3
lead_w_email -------------lead---seq=3
lead_w_partner -----------lead---seq=False
"""
# ensure initial data
(self.lead_w_partner_company | self.lead_1).write({'type': 'opportunity', 'probability': 50})
leads = self.env['crm.lead'].browse(self.leads.ids)._sort_by_confidence_level(reverse=True)
# lead_w_partner is lost, check that the "lost_reason" is not propagated
# because "lead_1" is not lost
lost_reason = self.env['crm.lost.reason'].create({'name': 'Test Reason'})
self.lead_w_partner.write({
'lost_reason_id': lost_reason,
'probability': 0,
})
all_tags = self.leads.mapped('tag_ids')
with self.assertLeadMerged(self.lead_1, leads,
name='Nibbler Spacecraft Request',
partner_id=self.contact_company_1,
priority='2',
lost_reason_id=False,
tag_ids=all_tags):
leads._merge_opportunity(auto_unlink=False, max_length=None)
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_merge_method_propagate_lost_reason(self):
"""Check that the lost reason is propagated to the final lead if it's lost."""
self.leads.write({
'probability': 0,
'automated_probability': 50, # Do not automatically update the probability
})
lost_reason = self.env['crm.lost.reason'].create({'name': 'Test Reason'})
self.lead_w_partner.lost_reason_id = lost_reason
leads = self.env['crm.lead'].browse(self.leads.ids)._sort_by_confidence_level(reverse=True)
with self.assertLeadMerged(leads[0], leads, lost_reason_id=lost_reason):
leads._merge_opportunity(auto_unlink=False, max_length=None)
@users('user_sales_manager')
def test_lead_merge_properties_formatting(self):
lead = self.lead_1
partners = self.env['res.partner'].create([{'name': 'Alice'}, {'name': 'Bob'}])
lead.lead_properties = [{
'type': 'many2one',
'comodel': 'res.partner',
'name': 'test_many2one',
'string': 'My Partner',
'value': partners[0].id,
'definition_changed': True,
}, {
'type': 'many2many',
'comodel': 'res.partner',
'name': 'test_many2many',
'string': 'My Partners',
'value': partners.ids,
}, {
'type': 'selection',
'selection': [['a', 'A'], ['b', 'B']],
'name': 'test_selection',
'string': 'My Selection',
'value': 'a',
}, {
'type': 'tags',
'tags': [['a', 'A', 1], ['b', 'B', 2], ['c', 'C', 3]],
'name': 'test_tags',
'string': 'My Tags',
'value': ['a', 'c'],
}, {
'type': 'boolean',
'name': 'test_boolean',
'string': 'My Boolean',
'value': True,
}, {
'type': 'integer',
'name': 'test_integer',
'string': 'My Integer',
'value': 1337,
}, {
'type': 'datetime',
'name': 'test_datetime',
'string': 'My Datetime',
'value': '2022-02-21 16:11:42',
}]
expected = [{
'label': 'My Partner',
'value': 'Alice',
}, {
'label': 'My Partners',
'values': [
{'name': 'Alice'},
{'name': 'Bob'},
],
}, {
'label': 'My Selection',
'value': 'A',
}, {
'label': 'My Tags',
'values': [
{'name': 'A', 'color': 1},
{'name': 'C', 'color': 3},
],
}, {
'label': 'My Boolean',
'value': 'Yes',
}, {
'label': 'My Integer',
'value': 1337,
}, {
'label': 'My Datetime',
# datetime are stored as string because they are not JSONifiable
'value': '2022-02-21 16:11:42',
}]
self.assertEqual(expected, lead._format_properties())
# check the rendered template
result = self.env['ir.qweb']._render(
'crm.crm_lead_merge_summary',
{'opportunities': lead, 'is_html_empty': lambda x: True})
self.assertIn("o_tag_color_1", result)
self.assertIn("Alice", result)
self.assertIn("Bob", result)
@users('user_sales_manager')
def test_merge_method_dependencies(self):
""" Test if dependences for leads are not lost while merging leads. In
this test leads are ordered as
lead_w_partner_company ---opp----seq=1 (ID greater)
lead_w_contact -----------lead---seq=3
lead_w_email -------------lead---seq=3----------------attachments
lead_1 -------------------lead---seq=1----------------activity+meeting
lead_w_partner -----------lead---seq=False
"""
self.env['crm.lead'].browse(self.lead_w_partner_company.ids).write({'type': 'opportunity'})
# create side documents
attachments = self.env['ir.attachment'].create([
{'name': '%02d.txt' % idx,
'datas': base64.b64encode(b'Att%02d' % idx),
'res_model': 'crm.lead',
'res_id': self.lead_w_email.id,
} for idx in range(4)
])
lead_1 = self.env['crm.lead'].browse(self.lead_1.ids)
activity = lead_1.activity_schedule('crm.lead_test_activity_1')
calendar_event = self.env['calendar.event'].create({
'name': 'Meeting with partner',
'activity_ids': [(4, activity.id)],
'start': '2021-06-12 21:00:00',
'stop': '2021-06-13 00:00:00',
'res_model_id': self.env['ir.model']._get('crm.crm_lead').id,
'res_id': lead_1.id,
'opportunity_id': lead_1.id,
})
# run merge and check documents are moved to the master record
merge = self.env['crm.merge.opportunity'].with_context({
'active_model': 'crm.lead',
'active_ids': self.leads.ids,
'active_id': False,
}).create({
'team_id': self.sales_team_convert.id,
'user_id': False,
})
result = merge.action_merge()
master_lead = self.leads.filtered(lambda lead: lead.id == result['res_id'])
# check result of merge process
self.assertEqual(master_lead, self.lead_w_partner_company)
# records updated
self.assertEqual(calendar_event.opportunity_id, master_lead)
self.assertEqual(calendar_event.res_id, master_lead.id)
self.assertTrue(all(att.res_id == master_lead.id for att in attachments))
# 2many accessors updated
self.assertEqual(master_lead.activity_ids, activity)
self.assertEqual(master_lead.calendar_event_ids, calendar_event)
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_merge_method_followers(self):
""" Test that the followers of the leads are added in the destination lead.
They should be added if:
- The related partner was active on the lead (posted a message in the last 30 days)
- The related partner is not already following the destination lead
Leads Followers Info
---------------------------------------------------------------------------------
lead_w_contact contact_1 OK (destination lead)
lead_w_email contact_1 KO (already following the destination lead)
contact_2 OK (active on lead_w_email)
contact_company KO (most recent message on lead_w_email is 35 days ago, message
on lead_w_partner is not counted as they don't follow it)
lead_w_partner contact_2 KO (already added with lead_w_email)
lead_w_partner_company
"""
self.leads.message_follower_ids.unlink()
self.leads.message_ids.unlink()
self.lead_w_contact.message_subscribe([self.contact_1.id])
self.lead_w_email.message_subscribe([self.contact_1.id, self.contact_2.id, self.contact_company.id])
self.lead_w_partner.message_subscribe([self.contact_2.id])
self.env['mail.message'].create([{
'author_id': self.contact_1.id,
'model': 'crm.lead',
'res_id': self.lead_w_contact.id,
'date': Datetime.now() - timedelta(days=1),
'subtype_id': self.ref('mail.mt_comment'),
'reply_to': False,
'body': 'Test follower',
}, {
'author_id': self.contact_1.id,
'model': 'crm.lead',
'res_id': self.lead_w_email.id,
'date': Datetime.now() - timedelta(days=20),
'subtype_id': self.ref('mail.mt_comment'),
'reply_to': False,
'body': 'Test follower',
}, {
'author_id': self.contact_2.id,
'model': 'crm.lead',
'res_id': self.lead_w_email.id,
'date': Datetime.now() - timedelta(days=15),
'subtype_id': self.ref('mail.mt_comment'),
'reply_to': False,
'body': 'Test follower',
}, {
'author_id': self.contact_2.id,
'model': 'crm.lead',
'res_id': self.lead_w_partner.id,
'date': Datetime.now() - timedelta(days=29),
'subtype_id': self.ref('mail.mt_comment'),
'reply_to': False,
'body': 'Test follower',
}, {
'author_id': self.contact_company.id,
'model': 'crm.lead',
'res_id': self.lead_w_email.id,
'date': Datetime.now() - timedelta(days=35),
'subtype_id': self.ref('mail.mt_comment'),
'reply_to': False,
'body': 'Test follower',
}, {
'author_id': self.contact_company.id,
'model': 'crm.lead',
'res_id': self.lead_w_partner.id,
'date': Datetime.now(),
'subtype_id': self.ref('mail.mt_comment'),
'reply_to': False,
'body': 'Test follower',
}])
initial_followers = self.lead_w_contact.message_follower_ids
leads = self.env['crm.lead'].browse(self.leads.ids)._sort_by_confidence_level(reverse=True)
master_lead = leads._merge_opportunity(max_length=None)
self.assertEqual(master_lead, self.lead_w_contact)
# Check followers of the destination lead
new_partner_followers = (master_lead.message_follower_ids - initial_followers).partner_id
self.assertIn(self.contact_2, new_partner_followers,
'The partner must follow the destination lead')
# "contact_company" posted a message 35 days ago on lead_2, so it's considered as inactive
# "contact_company" posted a message now on lead_3, but they don't follow lead_3
# so this message is just ignored
self.assertNotIn(self.contact_company, new_partner_followers,
'The partner was not active on the lead')
self.assertIn(self.contact_1, master_lead.message_follower_ids.partner_id,
'Should not have removed follower of the destination lead')

View file

@ -0,0 +1,322 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests.common import TestCrmCommon, INCOMING_EMAIL
from odoo.exceptions import AccessError, UserError
from odoo.tests import Form, tagged
from odoo.tests.common import users
@tagged('multi_company')
class TestCRMLeadMultiCompany(TestCrmCommon):
@classmethod
def setUpClass(cls):
super(TestCRMLeadMultiCompany, cls).setUpClass()
cls._activate_multi_company()
def test_initial_data(self):
""" Ensure global data for those tests to avoid unwanted side effects """
self.assertFalse(self.sales_team_1.company_id)
self.assertEqual(self.user_sales_manager_mc.company_id, self.company_2)
@users('user_sales_manager_mc')
def test_lead_mc_company_computation(self):
""" Test lead company computation depending on various parameters. Check
the company is set from the team_id or from the env if there is no team.
No responsible, no team, should not limit company. """
# Lead with falsy values are kept
lead_no_team = self.env['crm.lead'].create({
'name': 'L1',
'team_id': False,
'user_id': False,
})
self.assertFalse(lead_no_team.company_id)
self.assertFalse(lead_no_team.team_id)
self.assertFalse(lead_no_team.user_id)
# Lead with team with company sets company
lead_team_c2 = self.env['crm.lead'].create({
'name': 'L2',
'team_id': self.team_company2.id,
'user_id': False,
})
self.assertEqual(lead_team_c2.company_id, self.company_2)
self.assertFalse(lead_team_c2.user_id)
# Update team wo company: reset lead company also
lead_team_c2.team_id = self.sales_team_1
self.assertFalse(lead_team_c2.company_id)
# Lead with global team has no company
lead_team_no_company = self.env['crm.lead'].create({
'name': 'No company',
'team_id': self.sales_team_1.id,
'user_id': False,
})
self.assertFalse(lead_no_team.company_id)
# Update team w company updates company
lead_team_no_company.team_id = self.team_company2
self.assertEqual(lead_team_no_company.company_id, self.company_2)
self.assertEqual(lead_team_no_company.team_id, self.team_company2)
@users('user_sales_manager_mc')
def test_lead_mc_company_computation_env_team_norestrict(self):
""" Check that the computed company is the one coming from the team even
when it's not in self.env.companies. This may happen when running the
Lead Assignment task. """
LeadUnsyncCids = self.env['crm.lead'].with_context(allowed_company_ids=[self.company_main.id])
self.assertEqual(LeadUnsyncCids.env.company, self.company_main)
self.assertEqual(LeadUnsyncCids.env.companies, self.company_main)
self.assertEqual(LeadUnsyncCids.env.user.company_id, self.company_2)
# multicompany raises if trying to create manually
with self.assertRaises(AccessError):
lead = LeadUnsyncCids.create({
'name': 'My Lead MC',
'team_id': self.team_company2.id
})
# simulate auto-creation through sudo (assignment-like)
lead = LeadUnsyncCids.sudo().create({
'name': 'My Lead MC',
'team_id': self.team_company2.id,
})
self.assertEqual(lead.company_id, self.company_2)
self.assertEqual(lead.team_id, self.team_company2)
self.assertEqual(lead.user_id, self.user_sales_manager_mc)
@users('user_sales_manager_mc')
def test_lead_mc_company_computation_env_user_restrict(self):
""" Check that the computed company is allowed (aka in self.env.companies).
If the assigned team has a set company, the lead has the same one. Otherwise, use one
allowed by user, preferentially choose env current company, user's company otherwise."""
# User is logged in company_main even their default company is company_2
LeadUnsyncCids = self.env['crm.lead'].with_context(allowed_company_ids=[self.company_main.id])
self.assertEqual(LeadUnsyncCids.env.company, self.company_main)
self.assertEqual(LeadUnsyncCids.env.companies, self.company_main)
self.assertEqual(LeadUnsyncCids.env.user.company_id, self.company_2)
# simulate auto-creation through sudo (assignment-like)
lead_1_auto = LeadUnsyncCids.sudo().create({
'name': 'My Lead MC 1 Auto',
})
self.assertEqual(lead_1_auto.team_id, self.sales_team_1,
'[Auto/1] First available team in current company should have been assigned (fallback as user in no team in Main Company).')
self.assertEqual(lead_1_auto.company_id, self.company_main,
'[Auto/1] Current company should be set on the lead as no company was assigned given by team and company is allowed for user.')
self.assertEqual(lead_1_auto.user_id, self.user_sales_manager_mc, '[Auto/1] Current user should have been assigned.')
# manual creation
lead_1_manual = LeadUnsyncCids.create({
'name': 'My Lead MC',
})
self.assertEqual(lead_1_manual.team_id, self.sales_team_1,
'[Auto/1] First available team in current company should have been assigned (fallback as user in no team in Main Company).')
self.assertEqual(lead_1_manual.company_id, self.company_main,
'[Auto/1] Current company should be set on the lead as no company was given by team and company is allowed for user.')
self.assertEqual(lead_1_manual.user_id, self.user_sales_manager_mc, '[Manual/1] Current user should have been assigned.')
# Logged on other company will use that one for the lead company with sales_team_2 as is assigned to company_2
LeadUnsyncCids = self.env['crm.lead'].with_context(allowed_company_ids=[self.company_main.id, self.company_2.id])
LeadUnsyncCids = LeadUnsyncCids.with_company(self.company_2)
self.assertEqual(LeadUnsyncCids.env.company, self.company_2)
lead_2_auto = LeadUnsyncCids.sudo().create({
'name': 'My Lead MC 2 Auto',
})
self.assertEqual(lead_2_auto.team_id, self.team_company2,
'[Auto/2] First available team user is a member of, in current company, should have been assigned.')
self.assertEqual(lead_2_auto.company_id, self.company_2,
'[Auto/2] Current company should be set on the lead as company was assigned on team.')
self.assertEqual(lead_2_auto.user_id, self.user_sales_manager_mc, '[Auto/2] Current user should have been assigned.')
lead_2_manual = LeadUnsyncCids.create({
'name': 'My Lead MC 2 Manual',
})
self.assertEqual(lead_2_manual.team_id, self.team_company2,
'[Manual/2] First available team user is a member of, in current company, should have been assigned.')
self.assertEqual(lead_2_manual.company_id, self.company_2,
'[Manual/2] Current company should be set on the lead as company was assigned on team.')
self.assertEqual(lead_2_manual.user_id, self.user_sales_manager_mc, '[Manual/2] Current user should have been assigned.')
# If assigned team has no company, use company
self.team_company2.write({'company_id': False})
lead_3_auto = LeadUnsyncCids.sudo().create({
'name': 'My Lead MC 3 Auto',
})
self.assertEqual(lead_3_auto.team_id, self.team_company2,
'[Auto/3] First available team user is a member of should have been assigned (fallback as no team with same company defined).')
self.assertEqual(lead_3_auto.company_id, self.company_2,
'[Auto/3] Current company should be set on the lead as no company was given by team and company is allowed for user.')
self.assertEqual(lead_3_auto.user_id, self.user_sales_manager_mc, '[Auto/3] Current user should have been assigned.')
lead_3_manual = LeadUnsyncCids.create({
'name': 'My Lead MC 3 Manual',
})
self.assertEqual(lead_3_manual.company_id, self.company_2,
'[Auto/3] First available team user is a member of should have been assigned (fallback as no team with same company defined).')
self.assertEqual(lead_3_manual.team_id, self.team_company2,
'[Auto/3] Current company should be set on the lead as no company was given by team and company is allowed for user.')
self.assertEqual(lead_3_manual.user_id, self.user_sales_manager_mc, '[Manual/3] Current user should have been assigned.')
# If all teams have no company and don't have user as member, the first sales team is used.
self.team_company2.write({'member_ids': [(3, self.user_sales_manager_mc.id)]})
lead_4_auto = LeadUnsyncCids.sudo().create({
'name': 'My Lead MC 4 Auto',
})
self.assertEqual(lead_4_auto.team_id, self.sales_team_1,
'[Auto/4] As no team has current user as member nor current company as company_id, first available team should have been assigned.')
self.assertEqual(lead_4_auto.company_id, self.company_2,
'[Auto/4] Current company should be set on the lead as no company was given by team and company is allowed for user.')
self.assertEqual(lead_4_auto.user_id, self.user_sales_manager_mc, '[Auto/4] Current user should have been assigned.')
lead_4_manual = LeadUnsyncCids.create({
'name': 'My Lead MC 4 Manual',
})
self.assertEqual(lead_4_manual.company_id, self.company_2,
'[Manual/4] As no team has current user as member nor current company as company_id, first available team should have been assigned.')
self.assertEqual(lead_4_manual.team_id, self.sales_team_1,
'[Manual/4] Current company should be set on the lead as no company was given by team and company is allowed for user.')
self.assertEqual(lead_4_manual.user_id, self.user_sales_manager_mc,
'[Manual/4] Current user should have been assigned.')
@users('user_sales_manager_mc')
def test_lead_mc_company_computation_partner_restrict(self):
""" Check company on partner limits the company on lead. As contacts may
be separated by company, lead with a partner should be limited to that
company. """
partner_c2 = self.partner_c2.with_env(self.env)
self.assertEqual(partner_c2.company_id, self.company_2)
lead = self.env['crm.lead'].create({
'partner_id': partner_c2.id,
'name': 'MC Partner, no company lead',
'user_id': False,
'team_id': False,
})
self.assertEqual(lead.company_id, self.company_2)
partner_main = self.env['res.partner'].create({
'company_id': self.company_main.id,
'email': 'partner_main@multicompany.example.com',
'name': 'Customer for Main',
})
lead.write({'partner_id': partner_main})
self.assertEqual(lead.company_id, self.company_main)
# writing current user on lead would imply putting its team and team's company
# on lead (aka self.company_2), and this clashes with company restriction on
# customer
with self.assertRaises(UserError):
lead.write({
'user_id': self.env.user,
})
@users('user_sales_manager_mc')
def test_lead_mc_company_form(self):
""" Test lead company computation using form view """
crm_lead_form = Form(self.env['crm.lead'])
crm_lead_form.name = "Test Lead"
# default values: current user, its team and therefore its company
self.assertEqual(crm_lead_form.company_id, self.company_2)
self.assertEqual(crm_lead_form.user_id, self.user_sales_manager_mc)
self.assertEqual(crm_lead_form.team_id, self.team_company2)
# remove user, team only
crm_lead_form.user_id = self.env['res.users']
self.assertEqual(crm_lead_form.company_id, self.company_2)
self.assertEqual(crm_lead_form.user_id, self.env['res.users'])
self.assertEqual(crm_lead_form.team_id, self.team_company2)
# remove team, user only
crm_lead_form.user_id = self.user_sales_manager_mc
crm_lead_form.team_id = self.env['crm.team']
self.assertEqual(crm_lead_form.company_id, self.company_2)
self.assertEqual(crm_lead_form.user_id, self.user_sales_manager_mc)
self.assertEqual(crm_lead_form.team_id, self.env['crm.team'])
# remove both: void company to ease assignment
crm_lead_form.user_id = self.env['res.users']
self.assertEqual(crm_lead_form.company_id, self.env['res.company'])
self.assertEqual(crm_lead_form.user_id, self.env['res.users'])
self.assertEqual(crm_lead_form.team_id, self.env['crm.team'])
# force company manually
crm_lead_form.company_id = self.company_2
lead = crm_lead_form.save()
# user_sales_manager cannot read it due to MC rules
with self.assertRaises(AccessError):
lead.with_user(self.user_sales_manager).read(['name'])
@users('user_sales_manager_mc')
def test_lead_mc_company_form_progressives_setup(self):
""" Specific bug reported at Task-2520276. Flow
0) The sales team have no company set
1) Create a lead without a user_id and a team_id
2) Assign a team to the lead
3) Assign a user_id
Goal: if no company is set on the sales team the lead at step 2 should
not have any company_id set. Previous behavior
1) set the company of the env.user
2) Keep the company of the lead
3) set the user company if the current company is not one of the allowed company of the user
Wanted behavior
1) leave the company empty
2) set the company of the team even if it's False (so erase the company if the team has no company set)
3) set the user company if the current company is not one of the allowed company of the user
"""
lead = self.env['crm.lead'].create({
'name': 'Test Progressive Setup',
'user_id': False,
'team_id': False,
})
crm_lead_form = Form(lead)
self.assertEqual(crm_lead_form.company_id, self.env['res.company'])
crm_lead_form.team_id = self.sales_team_1
self.assertEqual(crm_lead_form.company_id, self.env['res.company'])
crm_lead_form.user_id = self.env.user
# self.assertEqual(crm_lead_form.company_id, self.env['res.company']) # FIXME
self.assertEqual(crm_lead_form.company_id, self.company_2)
@users('user_sales_manager_mc')
def test_lead_mc_company_form_w_partner_id(self):
""" Test lead company computation with partner having a company. """
partner_c2 = self.partner_c2.with_env(self.env)
crm_lead_form = Form(self.env['crm.lead'])
crm_lead_form.name = "Test Lead"
crm_lead_form.user_id = self.user_sales_manager_mc
crm_lead_form.partner_id = partner_c2
self.assertEqual(crm_lead_form.company_id, self.company_2, 'Crm: company comes from sales')
self.assertEqual(crm_lead_form.team_id, self.team_company2, 'Crm: team comes from sales')
# reset sales: should not reset company, as partner constrains it
crm_lead_form.team_id = self.env['crm.team']
crm_lead_form.user_id = self.env['res.users']
# ensuring that company_id is not overwritten when the salesperson becomes empty (w\o any team_id)
self.assertEqual(crm_lead_form.company_id, self.company_2, 'Crm: company comes from partner')
def test_gateway_incompatible_company_error_on_incoming_email(self):
self.assertTrue(self.sales_team_1.alias_name)
self.assertFalse(self.sales_team_1.company_id)
customer_company = self.env['res.partner'].create({
'company_id': self.company_2.id,
'email': 'customer.another.company@test.customer.com',
'mobile': '+32455000000',
'name': 'InCompany Customer',
})
new_lead = self.format_and_process(
INCOMING_EMAIL,
customer_company.email,
'%s@%s' % (self.sales_team_1.alias_name, self.alias_domain),
subject='Team having partner in company',
target_model='crm.lead',
)
self.assertEqual(new_lead.company_id, self.company_2)
self.assertEqual(new_lead.email_from, customer_company.email)
self.assertEqual(new_lead.partner_id, customer_company)

View file

@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.tests import tagged, users
from odoo.tools import mute_logger
@tagged('mail_thread', 'mail_gateway')
class NewLeadNotification(TestCrmCommon):
@users('user_sales_manager')
def test_lead_message_get_suggested_recipient(self):
""" Test '_message_get_suggested_recipients' and its override in lead. """
partner_no_email = self.env['res.partner'].create({'name': 'Test Partner', 'email': False})
(
lead_format,
lead_multi,
lead_from,
lead_partner,
lead_partner_no_email,
lead_partner_no_email_with_cc
) = self.env['crm.lead'].create([
{
'email_from': '"New Customer" <new.customer.format@test.example.com>',
'name': 'Test Suggestion (email_from with format)',
'partner_name': 'Format Name',
'user_id': self.user_sales_leads.id,
}, {
'email_from': 'new.customer.multi.1@test.example.com, new.customer.2@test.example.com',
'name': 'Test Suggestion (email_from multi)',
'partner_name': 'Multi Name',
'user_id': self.user_sales_leads.id,
}, {
'email_from': 'new.customer.simple@test.example.com',
'name': 'Test Suggestion (email_from)',
'partner_name': 'Std Name',
'user_id': self.user_sales_leads.id,
}, {
'name': 'Test Suggestion (partner_id)',
'partner_id': self.contact_1.id,
'user_id': self.user_sales_leads.id,
}, {
'name': 'Test Suggestion (partner no email)',
'partner_id': partner_no_email.id
}, {
'name': 'Test Suggestion (partner no email with cc email)',
'partner_id': partner_no_email.id,
'email_cc': 'test_cc@odoo.com'
}
])
for lead, expected_suggested in zip(
lead_format + lead_multi + lead_from + lead_partner + lead_partner_no_email + lead_partner_no_email_with_cc,
[
[(False, '"New Customer" <new.customer.format@test.example.com>', None, 'Customer Email')],
[(False, '"Multi Name" <new.customer.multi.1@test.example.com,new.customer.2@test.example.com>', None, 'Customer Email')],
[(False, '"Std Name" <new.customer.simple@test.example.com>', None, 'Customer Email')],
[(self.contact_1.id, '"Philip J Fry" <philip.j.fry@test.example.com>', self.contact_1.lang, 'Customer')],
[(partner_no_email.id, 'Test Partner', partner_no_email.lang, 'Customer')],
[
(False, 'test_cc@odoo.com', None, 'CC Email'),
(partner_no_email.id, 'Test Partner', partner_no_email.lang, 'Customer')
]
]
):
with self.subTest(lead=lead, email_from=lead.email_from):
res = lead._message_get_suggested_recipients()[lead.id]
self.assertEqual(len(res), len(expected_suggested))
self.assertEqual(res, expected_suggested)
def test_new_lead_notification(self):
""" Test newly create leads like from the website. People and channels
subscribed to the Sales Team shoud be notified. """
# subscribe a partner and a channel to the Sales Team with new lead subtype
sales_team_1 = self.env['crm.team'].create({
'name': 'Test Sales Team',
'alias_name': 'test_sales_team',
})
subtype = self.env.ref("crm.mt_salesteam_lead")
sales_team_1.message_subscribe(partner_ids=[self.user_sales_manager.partner_id.id], subtype_ids=[subtype.id])
# Imitate what happens in the controller when somebody creates a new
# lead from the website form
lead = self.env["crm.lead"].with_context(mail_create_nosubscribe=True).sudo().create({
"contact_name": "Somebody",
"description": "Some question",
"email_from": "somemail@example.com",
"name": "Some subject",
"partner_name": "Some company",
"team_id": sales_team_1.id,
"phone": "+0000000000"
})
# partner and channel should be auto subscribed
self.assertIn(self.user_sales_manager.partner_id, lead.message_partner_ids)
msg = lead.message_ids[0]
self.assertIn(self.user_sales_manager.partner_id, msg.notified_partner_ids)
# The user should have a new unread message
lead_user = lead.with_user(self.user_sales_manager)
self.assertTrue(lead_user.message_needaction)
@mute_logger('odoo.addons.mail.models.mail_thread')
def test_new_lead_from_email_multicompany(self):
company0 = self.env.company
company1 = self.env['res.company'].create({'name': 'new_company'})
self.env.user.write({
'company_ids': [(4, company0.id, False), (4, company1.id, False)],
})
crm_team_model = self.env['ir.model'].search([('model', '=', 'crm.team')])
crm_lead_model = self.env['ir.model'].search([('model', '=', 'crm.lead')])
self.env["ir.config_parameter"].sudo().set_param("mail.catchall.domain", 'aqualung.com')
crm_team0 = self.env['crm.team'].create({
'name': 'crm team 0',
'company_id': company0.id,
})
crm_team1 = self.env['crm.team'].create({
'name': 'crm team 1',
'company_id': company1.id,
})
mail_alias0 = self.env['mail.alias'].create({
'alias_name': 'sale_team_0',
'alias_model_id': crm_lead_model.id,
'alias_parent_model_id': crm_team_model.id,
'alias_parent_thread_id': crm_team0.id,
'alias_defaults': "{'type': 'opportunity', 'team_id': %s}" % crm_team0.id,
})
mail_alias1 = self.env['mail.alias'].create({
'alias_name': 'sale_team_1',
'alias_model_id': crm_lead_model.id,
'alias_parent_model_id': crm_team_model.id,
'alias_parent_thread_id': crm_team1.id,
'alias_defaults': "{'type': 'opportunity', 'team_id': %s}" % crm_team1.id,
})
crm_team0.write({'alias_id': mail_alias0.id})
crm_team1.write({'alias_id': mail_alias1.id})
new_message0 = """MIME-Version: 1.0
Date: Thu, 27 Dec 2018 16:27:45 +0100
Message-ID: <blablabla0>
Subject: sale team 0 in company 0
From: A client <client_a@someprovider.com>
To: sale_team_0@aqualung.com
Content-Type: multipart/alternative; boundary="000000000000a47519057e029630"
--000000000000a47519057e029630
Content-Type: text/plain; charset="UTF-8"
--000000000000a47519057e029630
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
<div>A good message</div>
--000000000000a47519057e029630--
"""
new_message1 = """MIME-Version: 1.0
Date: Thu, 27 Dec 2018 16:27:45 +0100
Message-ID: <blablabla1>
Subject: sale team 1 in company 1
From: B client <client_b@someprovider.com>
To: sale_team_1@aqualung.com
Content-Type: multipart/alternative; boundary="000000000000a47519057e029630"
--000000000000a47519057e029630
Content-Type: text/plain; charset="UTF-8"
--000000000000a47519057e029630
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
<div>A good message bis</div>
--000000000000a47519057e029630--
"""
crm_lead0_id = self.env['mail.thread'].message_process('crm.lead', new_message0)
crm_lead1_id = self.env['mail.thread'].message_process('crm.lead', new_message1)
crm_lead0 = self.env['crm.lead'].browse(crm_lead0_id)
crm_lead1 = self.env['crm.lead'].browse(crm_lead1_id)
self.assertEqual(crm_lead0.team_id, crm_team0)
self.assertEqual(crm_lead1.team_id, crm_team1)
self.assertEqual(crm_lead0.company_id, company0)
self.assertEqual(crm_lead1.company_id, company1)

View file

@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, datetime
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.tests.common import tagged, users
@tagged('post_install', '-at_install')
class TestCRMLeadSmartCalendar(TestCrmCommon):
@classmethod
def setUpClass(cls):
super(TestCRMLeadSmartCalendar, cls).setUpClass()
# weekstart index : 7 (sunday), tz : UTC -4 / -5
cls.user_NY_en_US = mail_new_test_user(
cls.env, login='user_NY_en_US', lang='en_US', tz='America/New_York',
name='user_NY_en_US User', email='user_NY_en_US@test.example.com',
notification_type='inbox',
groups='sales_team.group_sale_salesman_all_leads,base.group_partner_manager,crm.group_use_lead')
# weekstart index : 1 (monday), tz : UTC+0
cls.env['res.lang']._activate_lang('pt_PT')
cls.user_UTC_pt_PT = mail_new_test_user(
cls.env, login='user_UTC_pt_PT', lang='pt_PT', tz='Europe/Lisbon',
name='user_UTC_pt_PT User', email='user_UTC_pt_PT@test.example.com',
notification_type='inbox',
groups='sales_team.group_sale_salesman_all_leads,base.group_partner_manager,crm.group_use_lead')
cls.next_year = datetime.now().year + 1
cls.calendar_meeting_1 = cls.env['calendar.event'].create({
'name': 'calendar_meeting_1',
'start': datetime(2020, 12, 13, 17),
'stop': datetime(2020, 12, 13, 22)})
cls.calendar_meeting_2 = cls.env['calendar.event'].create({
'name': 'calendar_meeting_2',
'start': datetime(2020, 12, 13, 2),
'stop': datetime(2020, 12, 13, 3)})
cls.calendar_meeting_3 = cls.env['calendar.event'].create({
'name': 'calendar_meeting_3',
'start': datetime(cls.next_year, 5, 4, 12),
'stop': datetime(cls.next_year, 5, 4, 13)})
cls.calendar_meeting_4 = cls.env['calendar.event'].create({
'name': 'calendar_meeting_4',
'allday': True,
'start': datetime(2020, 12, 6, 0, 0, 0),
'stop': datetime(2020, 12, 6, 23, 59, 59)})
cls.calendar_meeting_5 = cls.env['calendar.event'].create({
'name': 'calendar_meeting_5',
'start': datetime(2020, 12, 13, 8),
'stop': datetime(2020, 12, 13, 18),
'allday': True})
cls.calendar_meeting_6 = cls.env['calendar.event'].create({
'name': 'calendar_meeting_6',
'start': datetime(2020, 12, 12, 0),
'stop': datetime(2020, 12, 19, 0)})
@users('user_NY_en_US')
def test_meeting_view_parameters_1(self):
lead_smart_calendar_1 = self.env['crm.lead'].create({'name': 'Lead 1 - user_NY_en_US'})
mode, initial_date = lead_smart_calendar_1._get_opportunity_meeting_view_parameters()
self.assertEqual(mode, 'week')
self.assertEqual(initial_date, False)
self.calendar_meeting_1.write({'opportunity_id': lead_smart_calendar_1.id})
mode, initial_date = lead_smart_calendar_1._get_opportunity_meeting_view_parameters()
self.assertEqual(mode, 'week')
self.assertEqual(initial_date, date(2020, 12, 13))
self.calendar_meeting_2.write({'opportunity_id': lead_smart_calendar_1.id})
mode, initial_date = lead_smart_calendar_1._get_opportunity_meeting_view_parameters()
self.assertEqual(mode, 'month')
self.assertEqual(initial_date, date(2020, 12, 12))
self.calendar_meeting_3.write({'opportunity_id': lead_smart_calendar_1.id})
mode, initial_date = lead_smart_calendar_1._get_opportunity_meeting_view_parameters()
self.assertEqual(mode, 'week')
self.assertEqual(initial_date, date(self.next_year, 5, 4))
lead_smart_calendar_2 = self.env['crm.lead'].create({'name': 'Lead 2 - user_NY_en_US'})
self.calendar_meeting_4.write({'opportunity_id': lead_smart_calendar_2.id})
mode, initial_date = lead_smart_calendar_2._get_opportunity_meeting_view_parameters()
self.assertEqual(mode, 'week')
self.assertEqual(initial_date, date(2020, 12, 6))
self.calendar_meeting_2.write({'opportunity_id': lead_smart_calendar_2.id})
mode, initial_date = lead_smart_calendar_2._get_opportunity_meeting_view_parameters()
self.assertEqual(mode, 'week')
self.assertEqual(initial_date, date(2020, 12, 6))
self.calendar_meeting_5.write({'opportunity_id': lead_smart_calendar_2.id})
mode, initial_date = lead_smart_calendar_2._get_opportunity_meeting_view_parameters()
self.assertEqual(mode, 'month')
self.assertEqual(initial_date, date(2020, 12, 6))
@users('user_UTC_pt_PT')
def test_meeting_view_parameters_2(self):
lead_smart_calendar_1 = self.env['crm.lead'].create({'name': 'Lead 1 - user_UTC_pt_PT'})
self.calendar_meeting_1.write({'opportunity_id': lead_smart_calendar_1.id})
self.calendar_meeting_2.write({'opportunity_id': lead_smart_calendar_1.id})
mode, initial_date = lead_smart_calendar_1._get_opportunity_meeting_view_parameters()
self.assertEqual(mode, 'week')
self.assertEqual(initial_date, date(2020, 12, 13))
self.calendar_meeting_3.write({'opportunity_id': lead_smart_calendar_1.id})
mode, initial_date = lead_smart_calendar_1._get_opportunity_meeting_view_parameters()
self.assertEqual(mode, 'week')
self.assertEqual(initial_date, date(self.next_year, 5, 4))
lead_smart_calendar_2 = self.env['crm.lead'].create({'name': 'Lead 2 - user_UTC_pt_PT'})
self.calendar_meeting_6.write({'opportunity_id': lead_smart_calendar_2.id})
mode, initial_date = lead_smart_calendar_2._get_opportunity_meeting_view_parameters()
self.assertEqual(mode, 'week')
self.assertEqual(initial_date, date(2020, 12, 12))
@users('user_sales_leads')
def test_meeting_creation_from_lead_form(self):
""" When going from a lead to the Calendar and adding a meeting, both salesman and customer
should be attendees of the event """
lead = self.env['crm.lead'].create({
'name': 'SuperLead',
'partner_id': self.contact_1.id,
})
calendar_action = lead.action_schedule_meeting()
event = self.env['calendar.event'].with_context(calendar_action['context']).create({
'start': datetime(2020, 12, 13, 17),
'stop': datetime(2020, 12, 13, 22),
})
self.assertEqual(len(event.attendee_ids), 2)
self.assertIn(self.user_sales_leads.partner_id, event.attendee_ids.partner_id)
self.assertIn(self.contact_1, event.attendee_ids.partner_id)

View file

@ -0,0 +1,635 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import tools
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.fields import Date
from odoo.tests import Form, tagged, users, loaded_demo_data
from odoo.tests.common import TransactionCase
@tagged('crm_lead_pls')
class TestCRMPLS(TransactionCase):
@classmethod
def setUpClass(cls):
""" Keep a limited setup to ensure tests are not impacted by other
records created in CRM common. """
super(TestCRMPLS, cls).setUpClass()
cls.company_main = cls.env.user.company_id
cls.user_sales_manager = mail_new_test_user(
cls.env, login='user_sales_manager',
name='Martin PLS Sales Manager', email='crm_manager@test.example.com',
company_id=cls.company_main.id,
notification_type='inbox',
groups='sales_team.group_sale_manager,base.group_partner_manager',
)
cls.pls_team = cls.env['crm.team'].create({
'name': 'PLS Team',
})
# Ensure independance on demo data
cls.env['crm.lead'].with_context({'active_test': False}).search([]).unlink()
cls.env['crm.lead.scoring.frequency'].search([]).unlink()
cls.cr.flush()
def _get_lead_values(self, team_id, name_suffix, country_id, state_id, email_state, phone_state, source_id, stage_id):
return {
'name': 'lead_' + name_suffix,
'type': 'opportunity',
'state_id': state_id,
'email_state': email_state,
'phone_state': phone_state,
'source_id': source_id,
'stage_id': stage_id,
'country_id': country_id,
'team_id': team_id
}
def generate_leads_with_tags(self, tag_ids):
Lead = self.env['crm.lead']
team_id = self.env['crm.team'].create({
'name': 'blup',
}).id
leads_to_create = []
for i in range(150):
if i < 50: # tag 1
leads_to_create.append({
'name': 'lead_tag_%s' % str(i),
'tag_ids': [(4, tag_ids[0])],
'team_id': team_id
})
elif i < 100: # tag 2
leads_to_create.append({
'name': 'lead_tag_%s' % str(i),
'tag_ids': [(4, tag_ids[1])],
'team_id': team_id
})
else: # tag 1 and 2
leads_to_create.append({
'name': 'lead_tag_%s' % str(i),
'tag_ids': [(6, 0, tag_ids)],
'team_id': team_id
})
leads_with_tags = Lead.create(leads_to_create)
return leads_with_tags
def test_crm_lead_pls_update(self):
""" We test here that the wizard for updating probabilities from settings
is getting correct value from config params and after updating values
from the wizard, the config params are correctly updated
"""
# Set the PLS config
frequency_fields = self.env['crm.lead.scoring.frequency.field'].search([])
pls_fields_str = ','.join(frequency_fields.mapped('field_id.name'))
pls_start_date_str = "2021-01-01"
IrConfigSudo = self.env['ir.config_parameter'].sudo()
IrConfigSudo.set_param("crm.pls_start_date", pls_start_date_str)
IrConfigSudo.set_param("crm.pls_fields", pls_fields_str)
date_to_update = "2021-02-02"
fields_to_remove = frequency_fields.filtered(lambda f: f.field_id.name in ['source_id', 'lang_id'])
fields_after_updation_str = ','.join((frequency_fields - fields_to_remove).mapped('field_id.name'))
# Check that wizard to update lead probabilities has correct value set by default
pls_update_wizard = Form(self.env['crm.lead.pls.update'])
with pls_update_wizard:
self.assertEqual(Date.to_string(pls_update_wizard.pls_start_date), pls_start_date_str, 'Correct date is taken from config')
self.assertEqual(','.join([f.field_id.name for f in pls_update_wizard.pls_fields]), pls_fields_str, 'Correct fields are taken from config')
# Update the wizard values and check that config values and probabilities are updated accordingly
pls_update_wizard.pls_start_date = date_to_update
for field in fields_to_remove:
pls_update_wizard.pls_fields.remove(field.id)
pls_update_wizard0 = pls_update_wizard.save()
pls_update_wizard0.action_update_crm_lead_probabilities()
# Config params should have been updated
self.assertEqual(IrConfigSudo.get_param("crm.pls_start_date"), date_to_update, 'Correct date is updated in config')
self.assertEqual(IrConfigSudo.get_param("crm.pls_fields"), fields_after_updation_str, 'Correct fields are updated in config')
def test_predictive_lead_scoring(self):
""" We test here computation of lead probability based on PLS Bayes.
We will use 3 different values for each possible variables:
country_id : 1,2,3
state_id: 1,2,3
email_state: correct, incorrect, None
phone_state: correct, incorrect, None
source_id: 1,2,3
stage_id: 1,2,3 + the won stage
And we will compute all of this for 2 different team_id
Note : We assume here that original bayes computation is correct
as we don't compute manually the probabilities."""
Lead = self.env['crm.lead']
LeadScoringFrequency = self.env['crm.lead.scoring.frequency']
state_values = ['correct', 'incorrect', None]
source_ids = self.env['utm.source'].search([], limit=3).ids
state_ids = self.env['res.country.state'].search([], limit=3).ids
country_ids = self.env['res.country'].search([], limit=3).ids
stage_ids = self.env['crm.stage'].search([], limit=3).ids
won_stage_id = self.env['crm.stage'].search([('is_won', '=', True)], limit=1).id
team_ids = self.env['crm.team'].create([{'name': 'Team Test 1'}, {'name': 'Team Test 2'}, {'name': 'Team Test 3'}]).ids
# create bunch of lost and won crm_lead
leads_to_create = []
# for team 1
for i in range(3):
leads_to_create.append(
self._get_lead_values(team_ids[0], 'team_1_%s' % str(i), country_ids[i], state_ids[i], state_values[i], state_values[i], source_ids[i], stage_ids[i]))
leads_to_create.append(
self._get_lead_values(team_ids[0], 'team_1_%s' % str(3), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[2], stage_ids[1]))
leads_to_create.append(
self._get_lead_values(team_ids[0], 'team_1_%s' % str(4), country_ids[1], state_ids[1], state_values[1], state_values[0], source_ids[1], stage_ids[0]))
# for team 2
leads_to_create.append(
self._get_lead_values(team_ids[1], 'team_2_%s' % str(5), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[1], stage_ids[2]))
leads_to_create.append(
self._get_lead_values(team_ids[1], 'team_2_%s' % str(6), country_ids[0], state_ids[1], state_values[0], state_values[1], source_ids[2], stage_ids[1]))
leads_to_create.append(
self._get_lead_values(team_ids[1], 'team_2_%s' % str(7), country_ids[0], state_ids[2], state_values[0], state_values[1], source_ids[2], stage_ids[0]))
leads_to_create.append(
self._get_lead_values(team_ids[1], 'team_2_%s' % str(8), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[2], stage_ids[1]))
leads_to_create.append(
self._get_lead_values(team_ids[1], 'team_2_%s' % str(9), country_ids[1], state_ids[0], state_values[1], state_values[0], source_ids[1], stage_ids[1]))
# for leads with no team
leads_to_create.append(
self._get_lead_values(False, 'no_team_%s' % str(10), country_ids[1], state_ids[1], state_values[2], state_values[0], source_ids[1], stage_ids[2]))
leads_to_create.append(
self._get_lead_values(False, 'no_team_%s' % str(11), country_ids[0], state_ids[1], state_values[1], state_values[1], source_ids[0], stage_ids[0]))
leads_to_create.append(
self._get_lead_values(False, 'no_team_%s' % str(12), country_ids[1], state_ids[2], state_values[0], state_values[1], source_ids[2], stage_ids[0]))
leads_to_create.append(
self._get_lead_values(False, 'no_team_%s' % str(13), country_ids[0], state_ids[1], state_values[2], state_values[0], source_ids[2], stage_ids[1]))
leads = Lead.create(leads_to_create)
# Assert lead data.
existing_leads = Lead.with_context({'active_filter': False}).search([])
self.assertEqual(existing_leads, leads)
self.assertEqual(existing_leads.filtered(lambda lead: not lead.team_id), leads[-4::])
# Assign leads without team to team 3 to compare probability
# as a separate team and the one with no team set. See below (*)
leads[-4::].team_id = team_ids[2]
# Set the PLS config
self.env['ir.config_parameter'].sudo().set_param("crm.pls_start_date", "2000-01-01")
self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", "country_id,state_id,email_state,phone_state,source_id,tag_ids")
# set leads as won and lost
# for Team 1
leads[0].action_set_lost()
leads[1].action_set_lost()
leads[2].action_set_won()
# for Team 2
leads[5].action_set_lost()
leads[6].action_set_lost()
leads[7].action_set_won()
# Leads with no team
leads[10].action_set_won()
leads[11].action_set_lost()
leads[12].action_set_lost()
# A. Test Full Rebuild
# rebuild frequencies table and recompute automated_probability for all leads.
Lead._cron_update_automated_probabilities()
# As the cron is computing and writing in SQL queries, we need to invalidate the cache
self.env.invalidate_all()
self.assertEqual(tools.float_compare(leads[3].automated_probability, 33.49, 2), 0)
self.assertEqual(tools.float_compare(leads[8].automated_probability, 7.74, 2), 0)
lead_13_team_3_proba = leads[13].automated_probability
self.assertEqual(tools.float_compare(lead_13_team_3_proba, 35.09, 2), 0)
# Probability for Lead with no teams should be based on all the leads no matter their team.
# De-assign team 3 and rebuilt frequency table and recompute.
# Proba should be different as "no team" is not considered as a separated team. (*)
leads[-4::].write({'team_id': False})
leads[-4::].flush_recordset()
Lead._cron_update_automated_probabilities()
lead_13_no_team_proba = leads[13].automated_probability
self.assertTrue(lead_13_team_3_proba != leads[13].automated_probability, "Probability for leads with no team should be different than if they where in their own team.")
self.assertAlmostEqual(lead_13_no_team_proba, 35.19, places=2)
# Test frequencies
lead_4_stage_0_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', stage_ids[0])])
lead_4_stage_won_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', won_stage_id)])
lead_4_country_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[4].country_id.id)])
lead_4_email_state_freq = LeadScoringFrequency.search([('team_id', '=', leads[4].team_id.id), ('variable', '=', 'email_state'), ('value', '=', str(leads[4].email_state))])
lead_9_stage_0_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', stage_ids[0])])
lead_9_stage_won_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'stage_id'), ('value', '=', won_stage_id)])
lead_9_country_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[9].country_id.id)])
lead_9_email_state_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'email_state'), ('value', '=', str(leads[9].email_state))])
self.assertEqual(lead_4_stage_0_freq.won_count, 1.1)
self.assertEqual(lead_4_stage_won_freq.won_count, 1.1)
self.assertEqual(lead_4_country_freq.won_count, 0.1)
self.assertEqual(lead_4_email_state_freq.won_count, 1.1)
self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1)
self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1)
self.assertEqual(lead_4_country_freq.lost_count, 1.1)
self.assertEqual(lead_4_email_state_freq.lost_count, 2.1)
self.assertEqual(lead_9_stage_0_freq.won_count, 1.1)
self.assertEqual(lead_9_stage_won_freq.won_count, 1.1)
self.assertEqual(lead_9_country_freq.won_count, 0.0) # frequency does not exist
self.assertEqual(lead_9_email_state_freq.won_count, 1.1)
self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1)
self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1)
self.assertEqual(lead_9_country_freq.lost_count, 0.0) # frequency does not exist
self.assertEqual(lead_9_email_state_freq.lost_count, 2.1)
# B. Test Live Increment
leads[4].action_set_lost()
leads[9].action_set_won()
# re-get frequencies that did not exists before
lead_9_country_freq = LeadScoringFrequency.search([('team_id', '=', leads[9].team_id.id), ('variable', '=', 'country_id'), ('value', '=', leads[9].country_id.id)])
# B.1. Test frequencies - team 1 should not impact team 2
self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # + 1
self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) # unchanged - consider stages with <= sequence when lost
self.assertEqual(lead_4_country_freq.lost_count, 2.1) # + 1
self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # + 1
self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # + 1
self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # + 1 - consider every stages when won
self.assertEqual(lead_9_country_freq.won_count, 1.1) # + 1
self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # + 1
self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged
self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged
self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged (did not exists before)
self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged
# Propabilities of other leads should not be impacted as only modified lead are recomputed.
self.assertEqual(tools.float_compare(leads[3].automated_probability, 33.49, 2), 0)
self.assertEqual(tools.float_compare(leads[8].automated_probability, 7.74, 2), 0)
self.assertEqual(leads[3].is_automated_probability, True)
self.assertEqual(leads[8].is_automated_probability, True)
# Restore -> Should decrease lost
leads[4].toggle_active()
self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # - 1
self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) # unchanged - consider stages with <= sequence when lost
self.assertEqual(lead_4_country_freq.lost_count, 1.1) # - 1
self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # - 1
self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # unchanged
self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # unchanged
self.assertEqual(lead_9_country_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # unchanged
self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged
self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged
self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged
self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged
# set to won stage -> Should increase won
leads[4].stage_id = won_stage_id
self.assertEqual(lead_4_stage_0_freq.won_count, 2.1) # + 1
self.assertEqual(lead_4_stage_won_freq.won_count, 2.1) # + 1
self.assertEqual(lead_4_country_freq.won_count, 1.1) # + 1
self.assertEqual(lead_4_email_state_freq.won_count, 2.1) # + 1
self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # unchanged
self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1) # unchanged
self.assertEqual(lead_4_country_freq.lost_count, 1.1) # unchanged
self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # unchanged
# Archive (was won, now lost) -> Should decrease won and increase lost
leads[4].toggle_active()
self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # - 1
self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # - 1
self.assertEqual(lead_4_country_freq.won_count, 0.1) # - 1
self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # - 1
self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # + 1
self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # consider stages with <= sequence when lostand as stage is won.. even won_stage lost_count is increased by 1
self.assertEqual(lead_4_country_freq.lost_count, 2.1) # + 1
self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # + 1
# Move to original stage -> Should do nothing (as lead is still lost)
leads[4].stage_id = stage_ids[0]
self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1) # unchanged
self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # unchanged
self.assertEqual(lead_4_country_freq.lost_count, 2.1) # unchanged
self.assertEqual(lead_4_email_state_freq.lost_count, 3.1) # unchanged
# Restore -> Should decrease lost - at the end, frequencies should be like first frequencyes tests (except for 0.0 -> 0.1)
leads[4].toggle_active()
self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # - 1
self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # unchanged - consider stages with <= sequence when lost
self.assertEqual(lead_4_country_freq.lost_count, 1.1) # - 1
self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # - 1
# Probabilities should only be recomputed after modifying the lead itself.
leads[3].stage_id = stage_ids[0] # probability should only change a bit as frequencies are almost the same (except 0.0 -> 0.1)
leads[8].stage_id = stage_ids[0] # probability should change quite a lot
# Test frequencies (should not have changed)
self.assertEqual(lead_4_stage_0_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_stage_won_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_country_freq.won_count, 0.1) # unchanged
self.assertEqual(lead_4_email_state_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1) # unchanged
self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1) # unchanged
self.assertEqual(lead_4_country_freq.lost_count, 1.1) # unchanged
self.assertEqual(lead_4_email_state_freq.lost_count, 2.1) # unchanged
self.assertEqual(lead_9_stage_0_freq.won_count, 2.1) # unchanged
self.assertEqual(lead_9_stage_won_freq.won_count, 2.1) # unchanged
self.assertEqual(lead_9_country_freq.won_count, 1.1) # unchanged
self.assertEqual(lead_9_email_state_freq.won_count, 2.1) # unchanged
self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1) # unchanged
self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1) # unchanged
self.assertEqual(lead_9_country_freq.lost_count, 0.1) # unchanged
self.assertEqual(lead_9_email_state_freq.lost_count, 2.1) # unchanged
# Continue to test probability computation
leads[3].probability = 40
self.assertEqual(leads[3].is_automated_probability, False)
self.assertEqual(leads[8].is_automated_probability, True)
self.assertEqual(tools.float_compare(leads[3].automated_probability, 20.87, 2), 0)
self.assertEqual(tools.float_compare(leads[8].automated_probability, 2.43, 2), 0)
self.assertEqual(tools.float_compare(leads[3].probability, 40, 2), 0)
self.assertEqual(tools.float_compare(leads[8].probability, 2.43, 2), 0)
# Test modify country_id
leads[8].country_id = country_ids[1]
self.assertEqual(tools.float_compare(leads[8].automated_probability, 34.38, 2), 0)
self.assertEqual(tools.float_compare(leads[8].probability, 34.38, 2), 0)
leads[8].country_id = country_ids[0]
self.assertEqual(tools.float_compare(leads[8].automated_probability, 2.43, 2), 0)
self.assertEqual(tools.float_compare(leads[8].probability, 2.43, 2), 0)
# ----------------------------------------------
# Test tag_id frequencies and probability impact
# ----------------------------------------------
tag_ids = self.env['crm.tag'].create([
{'name': "Tag_test_1"},
{'name': "Tag_test_2"},
]).ids
# tag_ids = self.env['crm.tag'].search([], limit=2).ids
leads_with_tags = self.generate_leads_with_tags(tag_ids)
leads_with_tags[:30].action_set_lost() # 60% lost on tag 1
leads_with_tags[31:50].action_set_won() # 40% won on tag 1
leads_with_tags[50:90].action_set_lost() # 80% lost on tag 2
leads_with_tags[91:100].action_set_won() # 20% won on tag 2
leads_with_tags[100:135].action_set_lost() # 70% lost on tag 1 and 2
leads_with_tags[136:150].action_set_won() # 30% won on tag 1 and 2
# tag 1 : won = 19+14 / lost = 30+35
# tag 2 : won = 9+14 / lost = 40+35
tag_1_freq = LeadScoringFrequency.search([('variable', '=', 'tag_id'), ('value', '=', tag_ids[0])])
tag_2_freq = LeadScoringFrequency.search([('variable', '=', 'tag_id'), ('value', '=', tag_ids[1])])
self.assertEqual(tools.float_compare(tag_1_freq.won_count, 33.1, 1), 0)
self.assertEqual(tools.float_compare(tag_1_freq.lost_count, 65.1, 1), 0)
self.assertEqual(tools.float_compare(tag_2_freq.won_count, 23.1, 1), 0)
self.assertEqual(tools.float_compare(tag_2_freq.lost_count, 75.1, 1), 0)
# Force recompute - A priori, no need to do this as, for each won / lost, we increment tag frequency.
Lead._cron_update_automated_probabilities()
self.env.invalidate_all()
lead_tag_1 = leads_with_tags[30]
lead_tag_2 = leads_with_tags[90]
lead_tag_1_2 = leads_with_tags[135]
self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 33.69, 2), 0)
self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0)
self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 28.05, 2), 0)
lead_tag_1.tag_ids = [(5, 0, 0)] # remove all tags
lead_tag_1_2.tag_ids = [(3, tag_ids[1], 0)] # remove tag 2
self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0)
self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0) # no impact
self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 33.69, 2), 0)
lead_tag_1.tag_ids = [(4, tag_ids[1])] # add tag 2
lead_tag_2.tag_ids = [(4, tag_ids[0])] # add tag 1
lead_tag_1_2.tag_ids = [(3, tag_ids[0]), (4, tag_ids[1])] # remove tag 1 / add tag 2
self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 23.51, 2), 0)
self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 28.05, 2), 0)
self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 23.51, 2), 0)
# go back to initial situation
lead_tag_1.tag_ids = [(3, tag_ids[1]), (4, tag_ids[0])] # remove tag 2 / add tag 1
lead_tag_2.tag_ids = [(3, tag_ids[0])] # remove tag 1
lead_tag_1_2.tag_ids = [(4, tag_ids[0])] # add tag 1
self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 33.69, 2), 0)
self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0)
self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 28.05, 2), 0)
# set email_state for each lead and update probabilities
leads.filtered(lambda lead: lead.id % 2 == 0).email_state = 'correct'
leads.filtered(lambda lead: lead.id % 2 == 1).email_state = 'incorrect'
Lead._cron_update_automated_probabilities()
self.env.invalidate_all()
self.assertEqual(tools.float_compare(leads[3].automated_probability, 4.21, 2), 0)
self.assertEqual(tools.float_compare(leads[8].automated_probability, 0.23, 2), 0)
# remove all pls fields
self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", False)
Lead._cron_update_automated_probabilities()
self.env.invalidate_all()
self.assertEqual(tools.float_compare(leads[3].automated_probability, 34.38, 2), 0)
self.assertEqual(tools.float_compare(leads[8].automated_probability, 50.0, 2), 0)
# check if the probabilities are the same with the old param
self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", "country_id,state_id,email_state,phone_state,source_id")
Lead._cron_update_automated_probabilities()
self.env.invalidate_all()
self.assertEqual(tools.float_compare(leads[3].automated_probability, 4.21, 2), 0)
self.assertEqual(tools.float_compare(leads[8].automated_probability, 0.23, 2), 0)
# remove tag_ids from the calculation
self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0)
self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 28.6, 2), 0)
self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 28.6, 2), 0)
lead_tag_1.tag_ids = [(5, 0, 0)] # remove all tags
lead_tag_2.tag_ids = [(4, tag_ids[0])] # add tag 1
lead_tag_1_2.tag_ids = [(3, tag_ids[1], 0)] # remove tag 2
self.assertEqual(tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0)
self.assertEqual(tools.float_compare(lead_tag_2.automated_probability, 28.6, 2), 0)
self.assertEqual(tools.float_compare(lead_tag_1_2.automated_probability, 28.6, 2), 0)
def test_predictive_lead_scoring_always_won(self):
""" The computation may lead scores close to 100% (or 0%), we check that pending
leads are always in the ]0-100[ range."""
Lead = self.env['crm.lead']
LeadScoringFrequency = self.env['crm.lead.scoring.frequency']
country_id = self.env['res.country'].search([], limit=1).id
stage_id = self.env['crm.stage'].search([], limit=1).id
team_id = self.env['crm.team'].create({'name': 'Team Test 1'}).id
# create two leads
leads = Lead.create([
self._get_lead_values(team_id, 'edge pending', country_id, False, False, False, False, stage_id),
self._get_lead_values(team_id, 'edge lost', country_id, False, False, False, False, stage_id),
self._get_lead_values(team_id, 'edge won', country_id, False, False, False, False, stage_id),
])
# set a new tag
leads.tag_ids = self.env['crm.tag'].create({'name': 'lead scoring edge case'})
# Set the PLS config
self.env['ir.config_parameter'].sudo().set_param("crm.pls_start_date", "2000-01-01")
# tag_ids can be used in versions newer than v14
self.env['ir.config_parameter'].sudo().set_param("crm.pls_fields", "country_id")
# set leads as won and lost
leads[1].action_set_lost()
leads[2].action_set_won()
# recompute
Lead._cron_update_automated_probabilities()
self.env.invalidate_all()
# adapt the probability frequency to have high values
# this way we are nearly sure it's going to be won
freq_stage = LeadScoringFrequency.search([('variable', '=', 'stage_id'), ('value', '=', str(stage_id))])
freq_tag = LeadScoringFrequency.search([('variable', '=', 'tag_id'), ('value', '=', str(leads.tag_ids.id))])
freqs = freq_stage + freq_tag
# check probabilities: won edge case
freqs.write({'won_count': 10000000, 'lost_count': 1})
leads._compute_probabilities()
self.assertEqual(tools.float_compare(leads[2].probability, 100, 2), 0)
self.assertEqual(tools.float_compare(leads[1].probability, 0, 2), 0)
self.assertEqual(tools.float_compare(leads[0].probability, 99.99, 2), 0)
# check probabilities: lost edge case
freqs.write({'won_count': 1, 'lost_count': 10000000})
leads._compute_probabilities()
self.assertEqual(tools.float_compare(leads[2].probability, 100, 2), 0)
self.assertEqual(tools.float_compare(leads[1].probability, 0, 2), 0)
self.assertEqual(tools.float_compare(leads[0].probability, 0.01, 2), 0)
def test_settings_pls_start_date(self):
# We test here that settings never crash due to ill-configured config param 'crm.pls_start_date'
set_param = self.env['ir.config_parameter'].sudo().set_param
str_date_8_days_ago = Date.to_string(Date.today() - timedelta(days=8))
resConfig = self.env['res.config.settings']
set_param("crm.pls_start_date", "2021-10-10")
res_config_new = resConfig.new()
self.assertEqual(Date.to_string(res_config_new.predictive_lead_scoring_start_date),
"2021-10-10", "If config param is a valid date, date in settings should match with config param")
set_param("crm.pls_start_date", "")
res_config_new = resConfig.new()
self.assertEqual(Date.to_string(res_config_new.predictive_lead_scoring_start_date),
str_date_8_days_ago, "If config param is empty, date in settings should be set to 8 days before today")
set_param("crm.pls_start_date", "One does not simply walk into system parameters to corrupt them")
res_config_new = resConfig.new()
self.assertEqual(Date.to_string(res_config_new.predictive_lead_scoring_start_date),
str_date_8_days_ago, "If config param is not a valid date, date in settings should be set to 8 days before today")
def test_pls_no_share_stage(self):
""" We test here the situation where all stages are team specific, as there is
a current limitation (can be seen in _pls_get_won_lost_total_count) regarding
the first stage (used to know how many lost and won there is) that requires
to have no team assigned to it."""
Lead = self.env['crm.lead']
team_id = self.env['crm.team'].create([{'name': 'Team Test'}]).id
self.env['crm.stage'].search([('team_id', '=', False)]).write({'team_id': team_id})
lead = Lead.create({'name': 'team', 'team_id': team_id, 'probability': 41.23})
Lead._cron_update_automated_probabilities()
self.assertEqual(tools.float_compare(lead.probability, 41.23, 2), 0)
self.assertEqual(tools.float_compare(lead.automated_probability, 0, 2), 0)
@users('user_sales_manager')
def test_team_unlink(self):
""" Test that frequencies are sent to "no team" when unlinking a team
in order to avoid losing too much informations. """
pls_team = self.env["crm.team"].browse(self.pls_team.ids)
# clean existing data
self.env["crm.lead.scoring.frequency"].sudo().search([('team_id', '=', False)]).unlink()
# existing no-team data
no_team = [
('stage_id', '1', 20, 10),
('stage_id', '2', 0.1, 0.1),
('stage_id', '3', 10, 0),
('country_id', '1', 10, 0.1),
]
self.env["crm.lead.scoring.frequency"].sudo().create([
{'variable': variable, 'value': value,
'won_count': won_count, 'lost_count': lost_count,
'team_id': False,
} for variable, value, won_count, lost_count in no_team
])
# add some frequencies to team to unlink
team = [
('stage_id', '1', 20, 10), # existing noteam
('country_id', '1', 0.1, 10), # existing noteam
('country_id', '2', 0.1, 0), # new but void
('country_id', '3', 30, 30), # new
]
existing_plsteam = self.env["crm.lead.scoring.frequency"].sudo().create([
{'variable': variable, 'value': value,
'won_count': won_count, 'lost_count': lost_count,
'team_id': pls_team.id,
} for variable, value, won_count, lost_count in team
])
pls_team.unlink()
final_noteam = [
('stage_id', '1', 40, 20),
('stage_id', '2', 0.1, 0.1),
('stage_id', '3', 10, 0),
('country_id', '1', 10, 10),
('country_id', '3', 30, 30),
]
self.assertEqual(
existing_plsteam.exists(), self.env["crm.lead.scoring.frequency"],
'Frequencies of unlinked teams should be unlinked (cascade)')
existing_noteam = self.env["crm.lead.scoring.frequency"].sudo().search([
('team_id', '=', False),
('variable', 'in', ['stage_id', 'country_id']),
])
for frequency in existing_noteam:
stat = next(item for item in final_noteam if item[0] == frequency.variable and item[1] == frequency.value)
self.assertEqual(frequency.won_count, stat[2])
self.assertEqual(frequency.lost_count, stat[3])
self.assertEqual(len(existing_noteam), len(final_noteam))

View file

@ -0,0 +1,117 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.tests import HttpCase
from odoo.tests.common import tagged, users
@tagged('post_install', '-at_install')
class TestUi(HttpCase):
def test_01_crm_tour(self):
# TODO: The tour is raising a JS error when selecting Brandon Freeman
# but with the demo data it succeeds to continue if there is already another lead
# in the pipe
brandon = self.env["res.partner"].create({
'name': 'Brandon Freeman',
'email': 'brandon.freeman55@example.com',
'phone': '(355)-687-3262',
})
self.env['crm.lead'].create({
'name': "Zizizbroken",
'type': 'opportunity',
'partner_id': brandon.id,
'stage_id': self.env.ref('crm.stage_lead1').id,
'user_id': self.env.ref('base.user_admin').id,
})
self.start_tour("/web", 'crm_tour', login="admin")
def test_02_crm_tour_rainbowman(self):
# we create a new user to make sure they get the 'Congrats on your first deal!'
# rainbowman message.
self.env['res.users'].create({
'name': 'Temporary CRM User',
'login': 'temp_crm_user',
'password': 'temp_crm_user',
'groups_id': [(6, 0, [
self.ref('base.group_user'),
self.ref('sales_team.group_sale_salesman')
])]
})
self.start_tour("/web", 'crm_rainbowman', login="temp_crm_user")
def test_03_crm_tour_forecast(self):
self.start_tour("/web", 'crm_forecast', login="admin")
def test_email_and_phone_propagation_edit_save(self):
"""Test the propagation of the email / phone on the partner.
If the partner has no email but the lead has one, it should be propagated
if we edit and save the lead form.
"""
self.env['crm.lead'].search([]).unlink()
user_admin = self.env['res.users'].search([('login', '=', 'admin')])
partner = self.env['res.partner'].create({'name': 'Test Partner'})
lead = self.env['crm.lead'].create({
'name': 'Test Lead Propagation',
'type': 'opportunity',
'user_id': user_admin.id,
'partner_id': partner.id,
'email_from': 'test@example.com',
'phone': '+32 494 44 44 44',
})
partner.email = False
partner.phone = False
# Check initial state
self.assertFalse(partner.email)
self.assertFalse(partner.phone)
self.assertEqual(lead.email_from, 'test@example.com')
self.assertEqual(lead.phone, '+32 494 44 44 44')
self.assertTrue(lead.partner_email_update)
self.assertTrue(lead.partner_phone_update)
self.start_tour('/web', 'crm_email_and_phone_propagation_edit_save', login='admin')
self.assertEqual(lead.email_from, 'test@example.com', 'Should not have changed the lead email')
self.assertEqual(lead.phone, '+32 494 44 44 44', 'Should not have changed the lead phone')
self.assertEqual(partner.email, 'test@example.com', 'Should have propagated the lead email on the partner')
self.assertEqual(partner.phone, '+32 494 44 44 44', 'Should have propagated the lead phone on the partner')
def test_email_and_phone_propagation_remove_email_and_phone(self):
"""Test the propagation of the email / phone on the partner.
If we remove the email and phone on the lead, it should be removed on the
partner. This test check that we correctly detect field values changes in JS
(aka undefined VS falsy).
"""
self.env['crm.lead'].search([]).unlink()
user_admin = self.env['res.users'].search([('login', '=', 'admin')])
partner = self.env['res.partner'].create({'name': 'Test Partner'})
lead = self.env['crm.lead'].create({
'name': 'Test Lead Propagation',
'type': 'opportunity',
'user_id': user_admin.id,
'partner_id': partner.id,
'email_from': 'test@example.com',
'phone': '+32 494 44 44 44',
})
# Check initial state
self.assertEqual(partner.email, 'test@example.com')
self.assertEqual(lead.phone, '+32 494 44 44 44')
self.assertEqual(lead.email_from, 'test@example.com')
self.assertEqual(lead.phone, '+32 494 44 44 44')
self.assertFalse(lead.partner_email_update)
self.assertFalse(lead.partner_phone_update)
self.start_tour('/web', 'crm_email_and_phone_propagation_remove_email_and_phone', login='admin')
self.assertFalse(lead.email_from, 'Should have removed the email')
self.assertFalse(lead.phone, 'Should have removed the phone')
self.assertFalse(partner.email, 'Should have removed the email')
self.assertFalse(partner.phone, 'Should have removed the phone')

View file

@ -0,0 +1,198 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import random
from odoo.addons.crm.tests.test_crm_lead_assignment import TestLeadAssignCommon
from odoo.tests.common import tagged
from odoo.tools import mute_logger
@tagged('lead_assign', 'crm_performance', 'post_install', '-at_install')
class TestLeadAssignPerf(TestLeadAssignCommon):
""" Test performances of lead assignment feature added in saas-14.2
Assign process is a random process: randomizing teams leads to searching,
assigning and de-duplicating leads in various order. As a lot of search
are implied during assign process query counters may vary from run to run.
"Heavy" performance test included here ranged from 6K to 6.3K queries. Either
we set high counters maximum which makes those tests less useful. Either we
avoid random if possible which is what we decided to do by setting the seed
of random in tests.
"""
@mute_logger('odoo.models.unlink', 'odoo.addons.crm.models.crm_team', 'odoo.addons.crm.models.crm_team_member')
def test_assign_perf_duplicates(self):
""" Test assign process with duplicates on partner. Allow to ensure notably
that de duplication is effectively performed. """
# fix the seed and avoid randomness
random.seed(1940)
leads = self._create_leads_batch(
lead_type='lead',
user_ids=[False],
partner_ids=[self.contact_1.id, self.contact_2.id, False, False, False],
count=200
)
# commit probability and related fields
leads.flush_recordset()
self.assertInitialData()
# assign probability to leads (bypass auto probability as purpose is not to test pls)
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
for idx in range(0, 5):
sliced_leads = leads[idx:len(leads):5]
for lead in sliced_leads:
lead.probability = (idx + 1) * 10 * ((int(lead.priority) + 1) / 2)
# commit probability and related fields
leads.flush_recordset()
# randomness: at least 1 query
with self.with_user('user_sales_manager'):
self.env['res.users'].has_group('base.group_user') # warmup the cache to avoid inconsistency between community an enterprise
with self.assertQueryCount(user_sales_manager=1266): # crm 1187
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2)
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
leads_st1 = leads.filtered_domain([('team_id', '=', self.sales_team_1.id)])
leads_stc = leads.filtered_domain([('team_id', '=', self.sales_team_convert.id)])
self.assertLessEqual(len(leads_st1), 128)
self.assertLessEqual(len(leads_stc), 96)
self.assertEqual(len(leads_st1) + len(leads_stc), len(leads)) # Make sure all lead are assigned
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 11) # 45 max on 2 days (3) + compensation (8.4)
self.assertMemberAssign(self.sales_team_1_m2, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_1_m3, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_convert_m1, 8) # 30 max on 15 (2) + compensation (5.6)
self.assertMemberAssign(self.sales_team_convert_m2, 15) # 60 max on 15 (4) + compsantion (11.2)
@mute_logger('odoo.models.unlink', 'odoo.addons.crm.models.crm_team', 'odoo.addons.crm.models.crm_team_member')
def test_assign_perf_no_duplicates(self):
# fix the seed and avoid randomness
random.seed(1945)
leads = self._create_leads_batch(
lead_type='lead',
user_ids=[False],
partner_ids=[False],
count=100
)
# commit probability and related fields
leads.flush_recordset()
self.assertInitialData()
# assign probability to leads (bypass auto probability as purpose is not to test pls)
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
for idx in range(0, 5):
sliced_leads = leads[idx:len(leads):5]
for lead in sliced_leads:
lead.probability = (idx + 1) * 10 * ((int(lead.priority) + 1) / 2)
# commit probability and related fields
leads.flush_recordset()
# randomness: at least 1 query
with self.with_user('user_sales_manager'):
with self.assertQueryCount(user_sales_manager=585): # crm 584
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2)
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
leads_st1 = leads.filtered_domain([('team_id', '=', self.sales_team_1.id)])
leads_stc = leads.filtered_domain([('team_id', '=', self.sales_team_convert.id)])
self.assertEqual(len(leads_st1) + len(leads_stc), 100)
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 11) # 45 max on 2 days (3) + compensation (8.4)
self.assertMemberAssign(self.sales_team_1_m2, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_1_m3, 4) # 15 max on 2 days (1) + compensation (2.8)
self.assertMemberAssign(self.sales_team_convert_m1, 8) # 30 max on 15 (2) + compensation (5.6)
self.assertMemberAssign(self.sales_team_convert_m2, 15) # 60 max on 15 (4) + compensation (11.2)
@mute_logger('odoo.models.unlink', 'odoo.addons.crm.models.crm_team', 'odoo.addons.crm.models.crm_team_member')
def test_assign_perf_populated(self):
""" Test assignment on a more high volume oriented test set in order to
have more insights on query counts. """
# fix the seed and avoid randomness
random.seed(1871)
# create leads enough to have interesting counters
_lead_count, _email_dup_count, _partner_count = 600, 50, 150
leads = self._create_leads_batch(
lead_type='lead',
user_ids=[False],
partner_count=_partner_count,
country_ids=[self.env.ref('base.be').id, self.env.ref('base.fr').id, False],
count=_lead_count,
email_dup_count=_email_dup_count)
# commit probability and related fields
leads.flush_recordset()
self.assertInitialData()
# assign for one month, aka a lot
self.env.ref('crm.ir_cron_crm_lead_assign').write({'interval_type': 'days', 'interval_number': 30})
# create a third team
sales_team_3 = self.env['crm.team'].create({
'name': 'Sales Team 3',
'sequence': 15,
'alias_name': False,
'use_leads': True,
'use_opportunities': True,
'company_id': False,
'user_id': False,
'assignment_domain': [('country_id', '!=', False)],
})
sales_team_3_m1 = self.env['crm.team.member'].create({
'user_id': self.user_sales_manager.id,
'crm_team_id': sales_team_3.id,
'assignment_max': 60,
'assignment_domain': False,
})
sales_team_3_m2 = self.env['crm.team.member'].create({
'user_id': self.user_sales_leads.id,
'crm_team_id': sales_team_3.id,
'assignment_max': 60,
'assignment_domain': False,
})
sales_team_3_m3 = self.env['crm.team.member'].create({
'user_id': self.user_sales_salesman.id,
'crm_team_id': sales_team_3.id,
'assignment_max': 15,
'assignment_domain': [('probability', '>=', 10)],
})
sales_teams = self.sales_teams | sales_team_3
self.assertEqual(sum(team.assignment_max for team in sales_teams), 300)
self.assertEqual(len(leads), 650)
# assign probability to leads (bypass auto probability as purpose is not to test pls)
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)]) # ensure order
for idx in range(0, 5):
sliced_leads = leads[idx:len(leads):5]
for lead in sliced_leads:
lead.probability = (idx + 1) * 10 * ((int(lead.priority) + 1) / 2)
# commit probability and related fields
leads.flush_recordset()
# randomness
with self.with_user('user_sales_manager'):
with self.assertQueryCount(user_sales_manager=6280): # crm 6226 / com 6276 / ent 6278
self.env['crm.team'].browse(sales_teams.ids)._action_assign_leads(work_days=30)
# teams assign
leads = self.env['crm.lead'].search([('id', 'in', leads.ids)])
self.assertEqual(leads.team_id, sales_teams)
self.assertEqual(leads.user_id, sales_teams.member_ids)
# salespersons assign
self.members.invalidate_model(['lead_month_count'])
self.assertMemberAssign(self.sales_team_1_m1, 45) # 45 max on one month
self.assertMemberAssign(self.sales_team_1_m2, 15) # 15 max on one month
self.assertMemberAssign(self.sales_team_1_m3, 15) # 15 max on one month
self.assertMemberAssign(self.sales_team_convert_m1, 30) # 30 max on one month
self.assertMemberAssign(self.sales_team_convert_m2, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m1, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m2, 60) # 60 max on one month
self.assertMemberAssign(sales_team_3_m3, 15) # 15 max on one month

View file

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.tests.common import Form
from odoo.tests import tagged, users
@tagged('res_partner')
class TestPartner(TestCrmCommon):
@users('user_sales_leads')
def test_parent_sync_sales_rep(self):
""" Test team_id / user_id sync from parent to children if the contact
is a person. Company children are not updated. """
contact_company = self.contact_company.with_env(self.env)
contact_company_1 = self.contact_company_1.with_env(self.env)
self.assertFalse(contact_company.team_id)
self.assertFalse(contact_company.user_id)
self.assertFalse(contact_company_1.team_id)
self.assertFalse(contact_company_1.user_id)
child = self.contact_1.with_env(self.env)
self.assertEqual(child.parent_id, self.contact_company_1)
self.assertFalse(child.team_id)
self.assertFalse(child.user_id)
# update comppany sales rep info
contact_company.user_id = self.env.uid
contact_company.team_id = self.sales_team_1.id
# change child parent: shold update sales rep info
child.parent_id = contact_company.id
self.assertEqual(child.user_id, self.env.user)
# test form tool
# <field name="team_id" groups="base.group_no_one"/>
with self.debug_mode():
partner_form = Form(self.env['res.partner'], 'base.view_partner_form')
partner_form.parent_id = contact_company
partner_form.company_type = 'person'
partner_form.name = 'Hermes Conrad'
self.assertEqual(partner_form.team_id, self.sales_team_1)
self.assertEqual(partner_form.user_id, self.env.user)
partner_form.parent_id = contact_company_1
self.assertEqual(partner_form.team_id, self.sales_team_1)
self.assertEqual(partner_form.user_id, self.env.user)
# test form tool
# <field name="team_id" groups="base.group_no_one"/>
with self.debug_mode():
partner_form = Form(self.env['res.partner'], 'base.view_partner_form')
# `parent_id` is invisible when `is_company` is True (`company_type == 'company'`)
# and parent_id is not set
# So, set a temporary `parent_id` before setting the contact as company
# to make `parent_id` visible in the interface while being a company
# <field name="parent_id"
# attrs="{
# 'invisible': [
# '|',
# '&amp;', ('is_company','=', True),('parent_id', '=', False),
# ('company_name', '!=', False),('company_name', '!=', '')
# ]
# }"
# />
partner_form.parent_id = contact_company_1
partner_form.company_type = 'company'
partner_form.parent_id = contact_company
partner_form.name = 'Mom Corp'
self.assertFalse(partner_form.team_id)
self.assertFalse(partner_form.user_id)

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import tests
from odoo.tests import HttpCase
from odoo.tests.common import users
from odoo.addons.sales_team.tests.common import SalesTeamCommon
@tests.tagged('post_install', '-at_install')
class TestUi(HttpCase, SalesTeamCommon):
@users('salesmanager')
def test_crm_team_members_mono_company(self):
""" Make sure you can create crm.team records with members in a mono-company scenario """
self.sale_manager.sudo().groups_id -= self.env.ref("base.group_multi_company")
self.env['ir.config_parameter'].sudo().set_param('sales_team.membership_multi', True)
self.start_tour("/", "create_crm_team_tour", login="salesmanager")
created_team = self.env["crm.team"].search([("name", "=", "My CRM Team")])
self.assertTrue(bool(created_team))
self.assertEqual(
created_team.member_ids,
self.sale_user | self.sale_manager
)