Files
odoo_source/addons/crm/tests/common.py
T
Thibault Delavallée 574dd61351 [HACK] crm: allow ordering on my activities deadline
RATIONALE

Activities are a way to organize our daily pipe. Notably in a sales pipeline
salesmen may have multiple leads with activities to perform in the next few
days. We would like to be able to have a list view displaying a list of leads
with "my" deadline, and to order leads based on this field.

PURPOSE

As activities are linked to records through a one2many and as my activities
are only a subset of those activities, it is impossible to store a field
on lead model. Indeed it would depend on the current user which makes storing
it impossible.

However ordering on a column in list view asks for a stored field. As it is
not possible, this commit proposes to hijack the web client and search of lead
model in order to allow it.

SPECIFICATIONS

Introduce a activity_date_deadline_my field, computed and searchable that
behaves like activity_date_deadline. It is filtered on current user activities
instead of being global to the record activities. "My" deadline is the earliest
deadline of my activities, even in the past.

Ordering through web client calls search_read with an order parameter set.
Search_read then calls search. In this commit we therefore override search
to intercept a search without count with an order on activity_date_deadline_my.
In that case we do the search in two steps.

First step: fill with deadline-based results

  * Perform a read_group on my activities to get a mapping lead_id / deadline
    Remember date_deadline is required, we always have a value for it. Only
    the earliest deadline per lead is kept.
  * Search leads linked to those activities that also match the asked domain
    and order from the original search request.
  * Results of that search will be at the top of returned results. Use limit
    None because we have to search all leads linked to activities as ordering
    on deadline is done in post processing.
  * Reorder them according to deadline asc or desc depending on original
    search ordering. Finally take only a subset of those leads to fill with
    results matching asked offset / limit.

Second step: fill with other results. If first step does not gives results
enough to match offset and limit parameters we fill with a search on other
leads. We keep the asked domain and ordering while filtering out already
scanned leads to keep a coherent results.

All other search and search_read are left untouched by this commit to avoid
side effects. Search_count is not affected by this commit.

As web client order availability is simply based on field being stored we
have to allow hijacking its value. A new option 'allow_order' is added that
display the order icon in list view column headers. It allows calling
search_read with an order based on the column but nothing more. Using it
on a not-stored field will give a warning log and ORM ignores it by default.

LIMITATIONS

At least following limitations apply

  * search on "my activities" gives a list of ids that is used for searching
    and ordering. It means that having too much activities on a given user can
    result in poor performances.
    -> we consider that activities assigned to a given user should be kept
    under control. Leads with activities for a given user should be a small
    subset of leads without activities.
  * ordering on activity_date_deadline_my always put those records on the top
    (deadline asc or desc) whatever the actual ordering of order items given
    to search. This is done to avoid complex implementation where deadline
    would be used to order records within groups ordered by another field.
    -> for example: order = 'priority DESC, activity_date_deadline_my ASC'
    actually behaves like 'activity_date_deadline_my ASC, priority DESC'.

WARNING

This implementation is a hack. It has been done by professionals. Do not try
to do it at home.

More seriously this is hackish and should be avoided. IN our case we consider
that the use of activities in sales pipeline is mainly driven by "my activities
deadline", which is not necessarily the case with longer processes like tasks.

Moreover user tests tend to show that this feature could be interesting. It
will improve quality of Odoo CRM and its adoption by improving its daily
use. A framework solution has to be found. Until this is done we keep this
hack and hope it is not duplicated in other applications.

LINKS

Task ID 2276011
Prepares Task ID 2243124 (My activities in CRM)
PR #52232

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
2020-06-11 15:17:52 +00:00

344 lines
13 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from odoo.addons.mail.tests.common import MailCase, mail_new_test_user
from odoo.addons.sales_team.tests.common import TestSalesCommon
from odoo.fields import Datetime
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):
@classmethod
def setUpClass(cls):
super(TestCrmCommon, cls).setUpClass()
cls._init_mail_gateway()
cls.sales_team_1.write({
'alias_name': 'sales.test',
'use_leads': True,
'use_opportunities': True,
})
(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,
})
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',
'country_id': cls.env.ref('base.us').id,
})
# 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()
cls.contact_company_1 = cls.env['res.partner'].create({
'name': 'Planet Express',
'email': 'planet.express@test.example.com',
'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': 'philip.j.fry@test.example.com',
'mobile': '+1 202 555 0122',
'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': 'turanga.leela@test.example.com',
'parent_id': False,
'is_company': False,
'street': 'Cookieville Minimum-Security Orphanarium',
'city': 'New New York',
'country_id': cls.env.ref('base.us').id,
'mobile': '+1 202 555 0999',
'zip': '97648',
})
def _create_leads_batch(self, lead_type='lead', count=10):
""" 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);
"""
types = ['lead', 'opportunity']
partners = [self.contact_1.id, self.contact_2.id, False]
leads_data = [{
'name': 'TestLead_%02d' % (x),
'type': lead_type if lead_type else types[x % 2],
'partner_id': partners[x % 3],
'priority': '%s' % (x % 3),
} for x in range(count)]
return self.env['crm.lead'].create(leads_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
"""
self.customer = self.env['res.partner'].create({
'name': 'Lead1 Email Customer',
'email': lead.email_from,
})
self.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,
})
# self.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(),
# })
self.lead_partner = self.env['crm.lead'].create({
'name': 'Duplicate: customer ID',
'type': 'lead',
'team_id': lead.team_id.id,
'partner_id': self.customer.id,
})
if create_opp:
self.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,
})
self.opp_lost.action_set_lost()
else:
self.opp_lost = self.env['crm.lead']
# self.assertEqual(self.lead_email_from.email_normalized, self.lead_email_normalized.email_normalized)
# self.assertTrue(lead.email_from != self.lead_email_normalized.email_from)
# self.assertFalse(self.opp_lost.active)
# new_lead = self.lead_email_from | self.lead_email_normalized | self.lead_partner | self.opp_lost
new_leads = self.lead_email_from | self.lead_partner | self.opp_lost
new_leads.flush() # compute notably probability
return new_leads
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----------------/
# user_sales_manager-----R----------------R
# user_sales_leads-------M----------------/
# user_sales_salesman----/----------------M
# 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',
'alias_name': False,
'use_leads': True,
'use_opportunities': True,
'company_id': False,
'user_id': cls.user_sales_manager.id,
'member_ids': [(4, cls.user_sales_salesman.id)],
})
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.crm_lead_dt_patcher.start()
@classmethod
def tearDownClass(cls):
cls.crm_lead_dt_patcher.stop()
super(TestLeadConvertCommon, cls).tearDownClass()
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----------------/
# user_sales_manager---------R----------------R
# user_sales_leads-----------M----------------/
# user_sales_leads_convert---/----------------M <-- NEW
# user_sales_salesman--------/----------------M
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.write({
'member_ids': [(4, cls.user_sales_leads_convert.id)]
})
cls.lead_w_partner = cls.env['crm.lead'].create({
'name': 'New1',
'type': 'lead',
'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.lead_w_partner_company = cls.env['crm.lead'].create({
'name': 'New1',
'type': 'lead',
'probability': 15,
'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',
})
cls.lead_w_contact = cls.env['crm.lead'].create({
'name': 'LeadContact',
'type': 'lead',
'probability': 15,
'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',
'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.lead_w_partner | cls.lead_w_partner_company | cls.lead_w_contact | cls.lead_w_email | cls.lead_w_email_lost).flush()