PURPOSE
Introduce an automated tool to create leads from event registrations. Events
are a powerful source of leads as they attract attention. This leads to leads
with good quality as they mark interest while gathering contact information
which allows to follow-up.
This merge aims at automating this process by automatically creating leads
based on user-defined rules.
SPECIFICATIONS
Add a new module event_crm. It allows to create leads automatically based on
event registrations. This is done based on rules defined in a new model
event_lead_rule.
Add a new menu "Lead Generation" in the configuration tab of an event. It
allows to create generation rules. Those are a set of conditions to generate
a lead with some pre-filled values as the type of lead, tags or salesperson.
SPECIFICATIONS: CREATION TYPE
There are two types of lead creation:
* per attendee: create a lead for each registration;
* per order: create a lead for a group of registrations;
The last one is only available through interface if it is possible to register
a group of attendees in one action (when event_sale or website_event are
installed). Behavior itself is implemented directly in event_crm.
Basically a group is either a list of registrations belonging to the same
event and created in batch (website_event flow). With event_sale this
definition will be improved to be based on sale_order.
SPECIFICATIONS: CREATION TRIGGERS
There are three options to trigger lead creation. We consider basically that
lead quality increases if attendees confirmed or went to the event. Triggers
allow therefore to run rules:
* at attendee creation;
* at attendee confirmation;
* at attendee venue;
This trigger defines when the rule will run.
SPECIFICATIONS: FILTERING REGISTRATIONS
When a batch of registrations matches the rule trigger we filter them based
on conditions and rules defines on event_lead_rule model. Heuristic is the
following:
* the rule is active;
* if a filter is set: filter registrations based on this filter. This is
done like a search, and filter is a domain;
* if a company is set on the rule, it must match event's company. Note
that multi-company rules apply on event_lead_rule;
* if an event category is set, it must match;
* if an event is set, it must match;
* if both event and category are set, one of them must match (OR). If none
of those are set, it is considered as passing;
If conditions are met, leads are created with pre-filled informations defined
on the rule (type, user_id, team_id). Contact information coming from the
registrations are computed (customer, name, email, phone, mobile, contact_name).
SPECIFICATIONS: OTHER POINTS
Note that all rules matching their conditions are applied. This means more
than one lead can be created depending on the configuration. This is
intended in order to give more freedom to the user using the automatic
lead generation.
Once a lead is created for an event registration, a stat button on the event
is available to show the number of leads generated for this event and to
be able to find them in one click.
Additionally on the lead form, a stat button will be display to show the
registrations linked to this lead. On registrations a stat button is also
displayed to find the leads created from it or from its group.
Most of code has been written to run in batch, in case multiples rules have
to run on a set of new registrations. Tests are added to ensure behavior.
LINKS
Task ID 2166679
PR #52334
Upgrade PR odoo/upgrade#1292
Co-Authored-By: Jérémy Hennecart <jeh@odoo.com>
Co-Authored-By: Thibault Delavallée <tde@odoo.com>
219 lines
11 KiB
Python
219 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from ast import literal_eval
|
|
from collections import defaultdict
|
|
|
|
from odoo import fields, models, _
|
|
from odoo.osv import expression
|
|
|
|
|
|
class EventLeadRule(models.Model):
|
|
""" Rule model for creating / updating leads from event registrations.
|
|
|
|
SPECIFICATIONS: CREATION TYPE
|
|
|
|
There are two types of lead creation:
|
|
|
|
* per attendee: create a lead for each registration;
|
|
* per order: create a lead for a group of registrations;
|
|
|
|
The last one is only available through interface if it is possible to register
|
|
a group of attendees in one action (when event_sale or website_event are
|
|
installed). Behavior itself is implemented directly in event_crm.
|
|
|
|
Basically a group is either a list of registrations belonging to the same
|
|
event and created in batch (website_event flow). With event_sale this
|
|
definition will be improved to be based on sale_order.
|
|
|
|
SPECIFICATIONS: CREATION TRIGGERS
|
|
|
|
There are three options to trigger lead creation. We consider basically that
|
|
lead quality increases if attendees confirmed or went to the event. Triggers
|
|
allow therefore to run rules:
|
|
|
|
* at attendee creation;
|
|
* at attendee confirmation;
|
|
* at attendee venue;
|
|
|
|
This trigger defines when the rule will run.
|
|
|
|
SPECIFICATIONS: FILTERING REGISTRATIONS
|
|
|
|
When a batch of registrations matches the rule trigger we filter them based
|
|
on conditions and rules defines on event_lead_rule model. Heuristic is the
|
|
following:
|
|
|
|
* the rule is active;
|
|
* if a filter is set: filter registrations based on this filter. This is
|
|
done like a search, and filter is a domain;
|
|
* if a company is set on the rule, it must match event's company. Note
|
|
that multi-company rules apply on event_lead_rule;
|
|
* if an event category it set, it must match;
|
|
* if an event is set, it must match;
|
|
* if both event and category are set, one of them must match (OR). If none
|
|
of those are set, it is considered as OK;
|
|
|
|
If conditions are met, leads are created with pre-filled informations defined
|
|
on the rule (type, user_id, team_id). Contact information coming from the
|
|
registrations are computed (customer, name, email, phone, mobile, contact_name).
|
|
|
|
SPECIFICATIONS: OTHER POINTS
|
|
|
|
Note that all rules matching their conditions are applied. This means more
|
|
than one lead can be created depending on the configuration. This is
|
|
intended in order to give more freedom to the user using the automatic
|
|
lead generation.
|
|
"""
|
|
_name = "event.lead.rule"
|
|
_description = "Event Lead Rules"
|
|
|
|
# Definition
|
|
name = fields.Char('Rule Name', required=True, translate=True)
|
|
active = fields.Boolean('Active', default=True)
|
|
lead_ids = fields.One2many(
|
|
'crm.lead', 'event_lead_rule_id', string='Created Leads',
|
|
groups='sales_team.group_sale_salesman')
|
|
# Triggers
|
|
lead_creation_basis = fields.Selection([
|
|
('attendee', 'Per Attendee'), ('order', 'Per Order')],
|
|
string='Create', default='attendee', required=True,
|
|
help='Per Attendee : A Lead is created for each Attendee (B2C).\n'
|
|
'Per Order : A single Lead is created per Ticket Batch/Sale Order (B2B)')
|
|
lead_creation_trigger = fields.Selection([
|
|
('create', 'Attendees are created'),
|
|
('confirm', 'Attendees are confirmed'),
|
|
('done', 'Attendees attended')],
|
|
string='When', default='create', required=True,
|
|
help='Creation: at attendee creation;\n'
|
|
'Confirmation: when attendee is confirmed, manually or automatically;\n'
|
|
'Attended: when attendance is confirmed and registration set to done;')
|
|
# Filters
|
|
event_type_ids = fields.Many2many(
|
|
'event.type', string='Event Categories',
|
|
help='Filter the attendees to include those of this specific event category. If not set, no event category restriction will be applied.')
|
|
event_id = fields.Many2one(
|
|
'event.event', string='Event',
|
|
domain="[('company_id', 'in', [company_id or current_company_id, False])]",
|
|
help='Filter the attendees to include those of this specific event. If not set, no event restriction will be applied.')
|
|
company_id = fields.Many2one(
|
|
'res.company', string='Company',
|
|
help="Restrict the trigger of this rule to events belonging to a specific company.\nIf not set, no company restriction will be applied.")
|
|
event_registration_filter = fields.Text(string="Registrations Domain", help="Filter the attendees that will or not generate leads.")
|
|
# Lead default_value fields
|
|
lead_type = fields.Selection([
|
|
('lead', 'Lead'), ('opportunity', 'Opportunity')], string="Lead Type", required=True,
|
|
default=lambda self: 'lead' if self.env['res.users'].has_group('crm.group_use_lead') else 'opportunity',
|
|
help="Default lead type when this rule is applied.")
|
|
lead_sales_team_id = fields.Many2one('crm.team', string='Sales Team', help="Automatically assign the created leads to this Sales Team.")
|
|
lead_user_id = fields.Many2one('res.users', string='Salesperson', help="Automatically assign the created leads to this Salesperson.")
|
|
lead_tag_ids = fields.Many2many('crm.tag', string='Tags', help="Automatically add these tags to the created leads.")
|
|
|
|
def _run_on_registrations(self, registrations):
|
|
""" Create or update leads based on rule configuration. Two main lead
|
|
management type exists
|
|
|
|
* per attendee: each registration creates a lead;
|
|
* per order: registrations are grouped per group and one lead is created
|
|
or updated with the batch (used mainly with sale order configuration
|
|
in event_sale);
|
|
|
|
Heuristic
|
|
|
|
* first, check existing lead linked to registrations to ensure no
|
|
duplication. Indeed for example attendee status change may trigger
|
|
the same rule several times;
|
|
* then for each rule, get the subset of registrations matching its
|
|
filters;
|
|
* then for each order-based rule, get the grouping information. This
|
|
give a list of registrations by group (event, sale_order), with maybe
|
|
an already-existing lead to update instead of creating a new one;
|
|
* finally apply rules. Attendee-based rules create a lead for each
|
|
attendee, group-based rules use the grouping information to create
|
|
or update leads;
|
|
|
|
:param registrations: event.registration recordset on which rules given by
|
|
self have to run. Triggers should already be checked, only filters are
|
|
applied here.
|
|
|
|
:return leads: newly-created leads. Updated leads are not returned.
|
|
"""
|
|
# order by ID, ensure first created wins
|
|
registrations = registrations.sorted('id')
|
|
|
|
# first: ensure no duplicate by searching existing registrations / rule
|
|
existing_leads = self.env['crm.lead'].search([
|
|
('registration_ids', 'in', registrations.ids),
|
|
('event_lead_rule_id', 'in', self.ids)
|
|
])
|
|
rule_to_existing_regs = defaultdict(lambda: self.env['event.registration'])
|
|
for lead in existing_leads:
|
|
rule_to_existing_regs[lead.event_lead_rule_id] += lead.registration_ids
|
|
|
|
# second: check registrations matching rules (in batch)
|
|
new_registrations = self.env['event.registration']
|
|
rule_to_new_regs = dict()
|
|
for rule in self:
|
|
new_for_rule = registrations.filtered(lambda reg: reg not in rule_to_existing_regs[rule])
|
|
rule_registrations = rule._filter_registrations(new_for_rule)
|
|
new_registrations |= rule_registrations
|
|
rule_to_new_regs[rule] = rule_registrations
|
|
new_registrations.sorted('id') # as an OR was used, re-ensure order
|
|
|
|
# third: check grouping
|
|
order_based_rules = self.filtered(lambda rule: rule.lead_creation_basis == 'order')
|
|
rule_group_info = new_registrations._get_lead_grouping(order_based_rules, rule_to_new_regs)
|
|
|
|
lead_vals_list = []
|
|
for rule in self:
|
|
if rule.lead_creation_basis == 'attendee':
|
|
matching_registrations = rule_to_new_regs[rule].sorted('id')
|
|
for registration in matching_registrations:
|
|
lead_vals_list.append(registration._get_lead_values(rule))
|
|
else:
|
|
# check if registrations are part of a group, for example a sale order, to know if we update or create leads
|
|
for (toupdate_leads, group_key, group_registrations) in rule_group_info[rule]:
|
|
if toupdate_leads:
|
|
additionnal_description = group_registrations._get_lead_description(_("New registrations"), line_counter=True)
|
|
for lead in toupdate_leads:
|
|
lead.write({
|
|
'description': "%s\n%s" % (lead.description, additionnal_description),
|
|
'registration_ids': [(4, reg.id) for reg in group_registrations],
|
|
})
|
|
else:
|
|
lead_vals_list.append(group_registrations._get_lead_values(rule))
|
|
|
|
return self.env['crm.lead'].create(lead_vals_list)
|
|
|
|
def _filter_registrations(self, registrations):
|
|
""" Keep registrations matching rule conditions. Those are
|
|
|
|
* if a filter is set: filter registrations based on this filter. This is
|
|
done like a search, and filter is a domain;
|
|
* if a company is set on the rule, it must match event's company. Note
|
|
that multi-company rules apply on event_lead_rule;
|
|
* if an event category it set, it must match;
|
|
* if an event is set, it must match;
|
|
* if both event and category are set, one of them must match (OR). If none
|
|
of those are set, it is considered as OK;
|
|
|
|
:param registrations: event.registration recordset on which rule filters
|
|
will be evaluated;
|
|
:return: subset of registrations matching rules
|
|
"""
|
|
self.ensure_one()
|
|
if self.event_registration_filter and self.event_registration_filter != '[]':
|
|
registrations = registrations.search(expression.AND([
|
|
[('id', 'in', registrations.ids)],
|
|
literal_eval(self.event_registration_filter)
|
|
]))
|
|
|
|
# check from direct m2o to linked m2o / o2m to filter first without inner search
|
|
company_ok = lambda registration: registration.company_id == self.company_id if self.company_id else True
|
|
event_or_event_type_ok = \
|
|
lambda registration: \
|
|
registration.event_id == self.event_id or registration.event_id.event_type_id in self.event_type_ids \
|
|
if (self.event_id or self.event_type_ids) else True
|
|
|
|
return registrations.filtered(lambda r: company_ok(r) and event_or_event_type_ok(r))
|