[IMP] crm_*: add more fields when we merge multiple leads
Purpose ======= Add more fields when we merge multiple leads to be sure not to lose valuable information. Specifications ============== New fields merged ----------------- Those fields are now propagated to the destination, - 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 - lost_reason (if the destination lead is lost) - iap_enrich_done - tag_ids The address is taken from the lead with the most non-empty address fields (sorted by highest rank if multiple leads have the same amount of non-empty fields). Followers --------- When merging multiple leads, we want to move the followers to the destination lead if they posted a message in the last 30 days. A message will be added in the merge note to know which followers have been added. Views ----- Remove the "groups" attribute on the recurring revenue fields. Instead we hide them directly in the views. Task-2447721 closes odoo/odoo#75742 Signed-off-by: awa-odoo <awa-odoo@users.noreply.github.com>
This commit is contained in:
@@ -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>
|
||||
@@ -99,7 +122,7 @@
|
||||
<div t-if="lead.state_id" t-field="lead.state_id"/>
|
||||
<div t-if="lead.country_id" t-field="lead.country_id"/>
|
||||
</div>
|
||||
<div class="mt-3 mb-3" name="marketing"
|
||||
<div class="mt-3" name="marketing"
|
||||
t-if="lead.campaign_id or lead.medium_id or lead.source_id">
|
||||
<div class="font-weight-bold">
|
||||
Marketing:
|
||||
@@ -114,6 +137,22 @@
|
||||
Source: <span t-field="lead.source_id"/>
|
||||
</div>
|
||||
</div>
|
||||
<t t-set="lead_followers" t-value="merged_followers and merged_followers.get(lead.id)"/>
|
||||
<div class="mt-3 mb-3" name="merged_followers" t-if="lead_followers">
|
||||
<div>
|
||||
The contacts below have been added as followers of this lead
|
||||
because they have been contacted less than 30 days ago on
|
||||
<span class="font-weight-bold" t-esc="lead.name"/>.
|
||||
</div>
|
||||
<ul>
|
||||
<li t-foreach="lead_followers" t-as="follower">
|
||||
<t t-esc="follower.partner_id.name"/>
|
||||
<t t-if="follower.partner_id.email">
|
||||
(<t t-esc="follower.partner_id.email"/>)
|
||||
</t>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</blockquote>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ from odoo.addons.phone_validation.tools import phone_validation
|
||||
from odoo.exceptions import UserError, AccessError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools.translate import _
|
||||
from odoo.tools import date_utils, email_re, email_split, is_html_empty
|
||||
from odoo.tools import date_utils, email_re, email_split, is_html_empty, groupby
|
||||
|
||||
from . import crm_stage
|
||||
|
||||
@@ -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
|
||||
@@ -146,14 +146,12 @@ class Lead(models.Model):
|
||||
# Revenues
|
||||
expected_revenue = fields.Monetary('Expected Revenue', currency_field='company_currency', tracking=True)
|
||||
prorated_revenue = fields.Monetary('Prorated Revenue', currency_field='company_currency', store=True, compute="_compute_prorated_revenue")
|
||||
recurring_revenue = fields.Monetary('Recurring Revenues', currency_field='company_currency', groups="crm.group_use_recurring_revenues", tracking=True)
|
||||
recurring_plan = fields.Many2one('crm.recurring.plan', string="Recurring Plan", groups="crm.group_use_recurring_revenues")
|
||||
recurring_revenue = fields.Monetary('Recurring Revenues', currency_field='company_currency', tracking=True)
|
||||
recurring_plan = fields.Many2one('crm.recurring.plan', string="Recurring Plan")
|
||||
recurring_revenue_monthly = fields.Monetary('Expected MRR', currency_field='company_currency', store=True,
|
||||
compute="_compute_recurring_revenue_monthly",
|
||||
groups="crm.group_use_recurring_revenues")
|
||||
compute="_compute_recurring_revenue_monthly")
|
||||
recurring_revenue_monthly_prorated = fields.Monetary('Prorated MRR', currency_field='company_currency', store=True,
|
||||
compute="_compute_recurring_revenue_monthly_prorated",
|
||||
groups="crm.group_use_recurring_revenues")
|
||||
compute="_compute_recurring_revenue_monthly_prorated")
|
||||
company_currency = fields.Many2one("res.currency", string='Currency', related='company_id.currency_id', readonly=True)
|
||||
# Dates
|
||||
date_closed = fields.Datetime('Closed Date', readonly=True, copy=False)
|
||||
@@ -1226,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):
|
||||
@@ -1246,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:
|
||||
@@ -1295,10 +1296,13 @@ class Lead(models.Model):
|
||||
if team_id:
|
||||
merged_data['team_id'] = team_id
|
||||
|
||||
merged_followers = opportunities_head._merge_followers(opportunities_tail)
|
||||
|
||||
# log merge message
|
||||
opportunities_head.message_post_with_view(
|
||||
"crm.crm_lead_merge_summary",
|
||||
values={
|
||||
"merged_followers": merged_followers,
|
||||
"opportunities": opportunities_tail,
|
||||
"is_html_empty": is_html_empty
|
||||
},
|
||||
@@ -1323,15 +1327,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,
|
||||
@@ -1412,6 +1437,50 @@ class Lead(models.Model):
|
||||
'opportunity_id': self.id,
|
||||
})
|
||||
|
||||
def _merge_followers(self, opportunities):
|
||||
"""Add the followers into the destination lead if they post a message in the last 30 days.
|
||||
|
||||
:param opportunities : Record<crm.lead> of opportunities to transfer
|
||||
:return: {old_lead_id: Record<mail.followers>} Followers which have been added in
|
||||
the destination lead grouped by source lead ID.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
self.env['mail.message'].flush()
|
||||
self.env['mail.followers'].flush()
|
||||
|
||||
# Get the active followers (followers whose partner post a message on the
|
||||
# leads in the last 30 days) which should be moved on the destination lead
|
||||
self.env.cr.execute(
|
||||
'''
|
||||
SELECT MAX(mf.id) AS id
|
||||
FROM mail_followers AS mf
|
||||
JOIN mail_message AS mm
|
||||
ON mm.author_id = mf.partner_id
|
||||
AND mm.res_id = mf.res_id
|
||||
AND mm.model = 'crm.lead'
|
||||
AND mm.date > NOW() - INTERVAL '30 DAY'
|
||||
/* Check if the partner is already
|
||||
following the destination lead */
|
||||
LEFT JOIN mail_followers AS destf
|
||||
ON destf.res_model = 'crm.lead'
|
||||
AND destf.res_id = %(lead_id)s
|
||||
AND destf.partner_id = mf.partner_id
|
||||
/* Select only once each partner
|
||||
to not create duplicated followers */
|
||||
WHERE mf.res_model = 'crm.lead'
|
||||
AND mf.res_id IN %(lead_ids)s
|
||||
AND destf IS NULL
|
||||
GROUP BY mf.partner_id
|
||||
''',
|
||||
{'lead_ids': tuple(opportunities.ids), 'lead_id': self.id},
|
||||
)
|
||||
followers_to_update = [r[0] for r in self.env.cr.fetchall()]
|
||||
followers_to_update = self.env['mail.followers'].browse(followers_to_update).sudo()
|
||||
followers_by_old_lead = dict(groupby(followers_to_update, lambda f: f.res_id))
|
||||
followers_to_update.write({'res_id': self.id})
|
||||
return followers_by_old_lead
|
||||
|
||||
# CONVERT
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# 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
|
||||
@@ -84,6 +85,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 +302,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')
|
||||
@@ -294,3 +392,97 @@ class TestLeadMerge(TestLeadMergeCommon):
|
||||
# 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 he doesn'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 he doesn'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')
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
<field name="expected_revenue" class="oe_inline mr-5" widget="monetary" options="{'currency_field': 'company_currency'}"/>
|
||||
<field name="priority" class="oe_inline" nolabel="1" widget="priority"/>
|
||||
</div>
|
||||
<div class="o_row">
|
||||
<div class="o_row" groups="crm.group_use_recurring_revenues">
|
||||
<field name="recurring_revenue" class="oe_inline pr-4" widget="monetary" options="{'currency_field': 'company_currency'}"/>
|
||||
<field name="recurring_plan" class="oe_inline" placeholder="E.g. Monthly"
|
||||
attrs="{'required': [('recurring_revenue', '!=', 0)]}" options="{'no_create': True, 'no_open': True}"/>
|
||||
@@ -559,11 +559,11 @@
|
||||
<div class="o_kanban_record_subtitle">
|
||||
<t t-if="record.expected_revenue.raw_value">
|
||||
<field name="expected_revenue" widget="monetary" options="{'currency_field': 'company_currency'}"/>
|
||||
<span t-if="record.recurring_revenue and record.recurring_revenue.raw_value"> + </span>
|
||||
<span t-if="record.recurring_revenue and record.recurring_revenue.raw_value" groups="crm.group_use_recurring_revenues"> + </span>
|
||||
</t>
|
||||
<t t-if="record.recurring_revenue and record.recurring_revenue.raw_value">
|
||||
<field name="recurring_revenue" widget="monetary" options="{'currency_field': 'company_currency'}"/>
|
||||
<field name="recurring_plan"/>
|
||||
<field name="recurring_revenue" widget="monetary" options="{'currency_field': 'company_currency'}" groups="crm.group_use_recurring_revenues"/>
|
||||
<field name="recurring_plan" groups="crm.group_use_recurring_revenues"/>
|
||||
</t>
|
||||
</div>
|
||||
<div>
|
||||
@@ -634,11 +634,11 @@
|
||||
<div class="o_kanban_record_subtitle">
|
||||
<t t-if="record.prorated_revenue.raw_value">
|
||||
<field name="prorated_revenue" widget="monetary" options="{'currency_field': 'company_currency'}"/>
|
||||
<span t-if="record.recurring_revenue and record.recurring_revenue.raw_value"> + </span>
|
||||
<span t-if="record.recurring_revenue and record.recurring_revenue.raw_value" groups="crm.group_use_recurring_revenues"> + </span>
|
||||
</t>
|
||||
<t t-if="record.recurring_revenue and record.recurring_revenue.raw_value">
|
||||
<field name="recurring_revenue" widget="monetary" options="{'currency_field': 'company_currency'}"/>
|
||||
<field name="recurring_plan"/>
|
||||
<field name="recurring_revenue" widget="monetary" options="{'currency_field': 'company_currency'}" groups="crm.group_use_recurring_revenues"/>
|
||||
<field name="recurring_plan" groups="crm.group_use_recurring_revenues"/>
|
||||
</t>
|
||||
</div>
|
||||
</xpath>
|
||||
@@ -772,9 +772,11 @@
|
||||
<field name="source_id" optional="hide"/>
|
||||
<field name="company_currency" invisible="1"/>
|
||||
<field name="expected_revenue" sum="Expected Revenues" optional="show" widget="monetary" options="{'currency_field': 'company_currency'}"/>
|
||||
<field name="recurring_revenue_monthly" sum="Expected MRR" optional="show" widget="monetary" options="{'currency_field': 'company_currency'}"/>
|
||||
<field name="recurring_revenue" sum="Recurring Revenue" optional="hide" widget="monetary" options="{'currency_field': 'company_currency'}"/>
|
||||
<field name="recurring_plan" optional="hide"/>
|
||||
<field name="recurring_revenue_monthly" sum="Expected MRR" optional="show" widget="monetary"
|
||||
options="{'currency_field': 'company_currency'}" groups="crm.group_use_recurring_revenues"/>
|
||||
<field name="recurring_revenue" sum="Recurring Revenue" optional="hide" widget="monetary"
|
||||
options="{'currency_field': 'company_currency'}" groups="crm.group_use_recurring_revenues"/>
|
||||
<field name="recurring_plan" optional="hide" groups="crm.group_use_recurring_revenues"/>
|
||||
<field name="stage_id" optional="show" decoration-bf="1"/>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="probability" string="Probability (%)" optional="hide"/>
|
||||
@@ -896,7 +898,7 @@
|
||||
<field name="create_date" interval="month" type="row"/>
|
||||
<field name="stage_id" type="col"/>
|
||||
<field name="expected_revenue" type="measure"/>
|
||||
<field name="recurring_revenue_monthly" type="measure"/>
|
||||
<field name="recurring_revenue_monthly" type="measure" groups="crm.group_use_recurring_revenues"/>
|
||||
<field name="color" invisible="1"/>
|
||||
</pivot>
|
||||
</field>
|
||||
|
||||
@@ -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,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)
|
||||
@@ -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']
|
||||
|
||||
@@ -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>
|
||||
@@ -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']
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user