[IMP] crm_*: add more fields when we merge multiple leads

Purpose
=======
Add more fields when we merge multiple leads to be sure not lose
valuable information.

Specifications
==============
Those fields are propagated to the destination if the value on the
destination is Falsy
- referred
- color
- recurring_revenue
- recurring_plan
- function
- lang_id
- date_deadline
- reveal_id
- lead_mining_request_id
- reveal_ip
- reveal_iap_credits
- reveal_rule_id
- event_lead_rule_id
- event_id

The "lost_reason" field is propagated to the destination only if it's
lost (otherwise, it makes no sense to have a lost reason on a non-lost
lead).

The field "iap_enrich_done" is set to True if at least one lead has been
enriched.

We also keep the sum of all the lead tags (and remove any potential
duplicates).

The address is taken from the lead with the most non-empty address
fields (sorted by highest rank if multiple lead have the same amount
of non-empty fields).

Task-2447721
PR odoo/odoo#75742
This commit is contained in:
std-odoo
2021-10-19 15:58:58 +00:00
parent 6e7b50708f
commit be996fdc00
17 changed files with 293 additions and 13 deletions
+24 -1
View File
@@ -40,15 +40,34 @@
<div t-if="lead.priority">
Priority: <span t-field="lead.priority"/>
</div>
<div t-if="lead.lost_reason">
Lost Reason: <span t-field="lead.lost_reason"/>
</div>
<div>
Created on: <span t-field="lead.create_date"/>
</div>
<div t-if="lead.date_action_last">
Last Action: <span t-field="lead.date_action_last"/>
</div>
<div t-if="lead.date_deadline">
Expected Closing: <span t-field="lead.date_deadline"/>
</div>
<div t-if="not is_html_empty(lead.description)">
Notes: <span t-field="lead.description"/>
</div>
<div t-if="lead.lang_id" name="lang_id">
Language: <span t-field="lead.lang_id"/>
</div>
<div t-if="lead.referred" name="referred">
Referred By: <span t-field="lead.referred"/>
</div>
<div t-if="lead.tag_ids" name="tag_ids" class="d-flex flex-row">
Tags:
<div class="ml-2 d-flex flex-row">
<div t-foreach="lead.tag_ids" t-as="tag" t-esc="tag.name"
t-attf-class="badge badge-pill o_tag_color_#{tag.color} d-inline-block"/>
</div>
</div>
<div t-if="lead.user_id" class="mt-3">
Salesperson: <span t-field="lead.user_id"/>
</div>
@@ -85,10 +104,14 @@
<div t-if="lead.website">
Website: <span t-field="lead.website"/>
</div>
<div t-if="lead.function">
Job Position: <span t-field="lead.function"/>
</div>
</div>
</div>
<div class="mt-3"
t-if="lead.street or lead.street2 or lead.zip or lead.city or lead.state_id or lead.country_id">
t-if="lead.street or lead.street2 or lead.zip or lead.city or lead.state_id or lead.country_id"
name="address">
<div class="font-weight-bold">
Address:
</div>
+32 -8
View File
@@ -33,31 +33,31 @@ CRM_LEAD_FIELDS_TO_MERGE = [
# description
'name',
'user_id',
'color',
'company_id',
'lang_id',
'team_id',
'referred',
# pipeline
'stage_id',
# revenues
'expected_revenue',
'recurring_plan',
'recurring_revenue',
# dates
'create_date',
'date_action_last',
'date_deadline',
# partner / contact
'partner_id',
'title',
'partner_name',
'contact_name',
'email_from',
'function',
'mobile',
'phone',
'website',
# address
'street',
'street2',
'zip',
'city',
'state_id',
'country_id',
]
# Subset of partner fields: sync any of those
@@ -1224,6 +1224,7 @@ class Lead(models.Model):
if fnames is None:
fnames = self._merge_get_fields()
fcallables = self._merge_get_fields_specific()
address_values = self._merge_get_fields_address()
# helpers
def _get_first_not_null(attr, opportunities):
@@ -1244,6 +1245,8 @@ class Lead(models.Model):
fcallable = fcallables.get(field_name)
if fcallable and callable(fcallable):
data[field_name] = fcallable(field_name, self)
elif field_name in address_values:
data[field_name] = address_values[field_name]
elif not fcallable and field.type in ('many2many', 'one2many'):
continue
else:
@@ -1321,15 +1324,36 @@ class Lead(models.Model):
return opportunities_head
def _merge_get_fields_address(self):
"""The address fields are propagated as a whole.
The address is taken from the lead with the most non-empty address field
(sorted by highest rank if multiple lead have the same amount of non-empty
fields).
"""
source_lead = max(self, key=lambda lead: len(list(
lead[field] for field in PARTNER_ADDRESS_FIELDS_TO_SYNC
if lead[field]
)))
return {fname: source_lead[fname] for fname in PARTNER_ADDRESS_FIELDS_TO_SYNC}
def _merge_get_fields_specific(self):
return {
'description': lambda fname, leads: '<br/><br/>'.join(desc for desc in leads.mapped('description') if not is_html_empty(desc)),
'type': lambda fname, leads: 'opportunity' if any(lead.type == 'opportunity' for lead in leads) else 'lead',
'priority': lambda fname, leads: max(leads.mapped('priority')) if leads else False,
'tag_ids': lambda fname, leads: leads.mapped('tag_ids'),
'lost_reason': lambda fname, leads:
False if leads and leads[0].probability
else next((lead.lost_reason for lead in leads if lead.lost_reason), False),
}
def _merge_get_fields(self):
return list(CRM_LEAD_FIELDS_TO_MERGE) + list(self._merge_get_fields_specific().keys())
return (
CRM_LEAD_FIELDS_TO_MERGE
+ list(self._merge_get_fields_specific().keys())
+ PARTNER_ADDRESS_FIELDS_TO_SYNC
)
def _merge_dependences(self, opportunities):
""" Merge dependences (messages, attachments,activities, calendar events,
+31 -1
View File
@@ -36,4 +36,34 @@
blockquote {
font-style: normal;
}
}
// Tag colors
@for $size from 1 through length($o-colors) {
.o_tag_color_#{$size - 1} {
border: transparent;
line-height: normal;
$background-color: white;
// no color selected
@if $size == 1 {
& {
color: black;
background-color: $background-color;
box-shadow: inset 0 0 0 1px nth($o-colors, $size);
}
} @else {
$background-color: nth($o-colors, $size);
& {
color: white;
background-color: $background-color;
}
}
@at-root a#{&} {
&:hover {
color: color-yiq($background-color);
background-color: darken($background-color, 10%);
}
}
}
}
}
+12 -2
View File
@@ -5,7 +5,7 @@ from ast import literal_eval
from contextlib import contextmanager
from unittest.mock import patch
from odoo.addons.crm.models.crm_lead import CRM_LEAD_FIELDS_TO_MERGE
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
@@ -423,7 +423,10 @@ class TestCrmCommon(TestSalesCommon, MailCase):
finally:
# support specific values caller may want to check in addition to generic tests
for fname, expected in expected.items():
self.assertEqual(opportunity[fname], expected)
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:
@@ -438,6 +441,9 @@ class TestCrmCommon(TestSalesCommon, MailCase):
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,
@@ -607,6 +613,9 @@ class TestLeadConvertMassCommon(TestLeadConvertCommon):
'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',
@@ -616,6 +625,7 @@ class TestLeadConvertMassCommon(TestLeadConvertCommon):
'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',
+98 -1
View File
@@ -84,6 +84,73 @@ class TestLeadMerge(TestLeadMergeCommon):
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):
@@ -234,10 +301,40 @@ class TestLeadMerge(TestLeadMergeCommon):
# 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': 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'):
priority='2',
lost_reason=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 = 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=lost_reason):
leads._merge_opportunity(auto_unlink=False, max_length=None)
@users('user_sales_manager')
+6
View File
@@ -159,3 +159,9 @@ class Lead(models.Model):
values=template_values,
subtype_id=self.env.ref('mail.mt_note').id
)
def _merge_get_fields_specific(self):
return {
** super(Lead, self)._merge_get_fields_specific(),
'iap_enrich_done': lambda fname, leads: any(lead.iap_enrich_done for lead in leads),
}
+1
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_crm_lead_merge
from . import test_lead_enrich
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm.tests.test_crm_lead_merge import TestLeadMergeCommon
from odoo.tests.common import tagged, users
from odoo.tools import mute_logger
@tagged('lead_manage')
class TestLeadMerge(TestLeadMergeCommon):
@users('user_sales_manager')
@mute_logger('odoo.models.unlink')
def test_merge_method_iap_enrich_done(self):
"""Test that the "iap_enrich_done" is set to True if at least one lead have this value True"""
self.leads.iap_enrich_done = False
self.lead_w_contact.write({
'reveal_id': 'test_reveal_id',
'iap_enrich_done': True,
})
leads = self.env['crm.lead'].browse(self.leads.ids)._sort_by_confidence_level(reverse=True)
with self.assertLeadMerged(leads[0], leads, iap_enrich_done=True, reveal_id='test_reveal_id'):
leads._merge_opportunity(auto_unlink=False, max_length=None)
+3
View File
@@ -8,3 +8,6 @@ class Lead(models.Model):
_inherit = 'crm.lead'
lead_mining_request_id = fields.Many2one('crm.iap.lead.mining.request', string='Lead Mining Request', index=True)
def _merge_get_fields(self):
return super(Lead, self)._merge_get_fields() + ['lead_mining_request_id']
+1
View File
@@ -11,6 +11,7 @@
'data': [
'security/event_crm_security.xml',
'security/ir.model.access.csv',
'data/crm_lead_merge_template.xml',
'views/crm_lead_views.xml',
'views/event_registration_views.xml',
'views/event_lead_rule_views.xml',
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="crm_lead_merge_summary_inherit_event_crm" inherit_id="crm.crm_lead_merge_summary">
<xpath expr="//div[@name='marketing']" position="after">
<div class="mt-3 mb-3" name="event" t-if="lead.event_lead_rule_id or lead.event_id">
<div class="font-weight-bold">
Event:
</div>
<div t-if="lead.event_id">
Event: <span t-field="lead.event_id"/>
</div>
<div t-if="lead.event_lead_rule_id">
Registration Rule: <span t-field="lead.event_lead_rule_id"/>
</div>
</div>
</xpath>
</template>
</odoo>
+3
View File
@@ -30,3 +30,6 @@ class Lead(models.Model):
self.sudo().write({
'registration_ids': [(4, registration.id) for registration in opportunities.sudo().registration_ids]
})
def _merge_get_fields(self):
return super(Lead, self)._merge_get_fields() + ['event_lead_rule_id', 'event_id']
+3
View File
@@ -8,3 +8,6 @@ class Lead(models.Model):
_inherit = 'crm.lead'
reveal_id = fields.Char(string='Reveal ID', help="Technical ID of reveal request done by IAP.")
def _merge_get_fields(self):
return super(Lead, self)._merge_get_fields() + ['reveal_id']
+1
View File
@@ -19,6 +19,7 @@ modules.
'depends': ['sale_management', 'crm'],
'data': [
'security/ir.model.access.csv',
'data/crm_lead_merge_template.xml',
'views/partner_views.xml',
'views/sale_order_views.xml',
'views/crm_lead_views.xml',
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="crm_lead_merge_summary_inherit_sale_crm" inherit_id="crm.crm_lead_merge_summary">
<xpath expr="//blockquote/*[last()]" position="after">
<div t-if="lead.order_ids">
<br/>
<div class="font-weight-bold">
Sale Orders
</div>
<ul>
<li t-foreach="lead.order_ids" t-as="order" t-esc="order.name"/>
</ul>
</div>
</xpath>
</template>
</odoo>
@@ -10,3 +10,6 @@ class Lead(models.Model):
reveal_ip = fields.Char(string='IP Address')
reveal_iap_credits = fields.Integer(string='IAP Credits')
reveal_rule_id = fields.Many2one('crm.reveal.rule', string='Lead Generation Rule', index=True)
def _merge_get_fields(self):
return super(Lead, self)._merge_get_fields() + ['reveal_ip', 'reveal_iap_credits', 'reveal_rule_id']
@@ -11,6 +11,17 @@
Partner Assignment Date: <span t-field="lead.date_partner_assign"/>
</div>
</xpath>
<xpath expr="//div[@name='address']" position="attributes">
<attribute name="t-if">
lead.street or lead.street2 or lead.zip or lead.city or lead.state_id or lead.country_id
or lead.partner_latitude or lead.partner_longitude
</attribute>
</xpath>
<xpath expr="//div[@name='address']/*[last()]" position="after">
<div name="partner_geolocation" t-if="lead.partner_latitude or lead.partner_longitude">
Geolocation: <t t-esc="lead.partner_latitude"/> latitude, <t t-esc="lead.partner_longitude"/> longitude
</div>
</xpath>
</template>
</odoo>