[MERGE][ADD] event_crm: create leads from attendees
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.
See sub commit for more detail.
LINKS
Task ID 2166679
PR odoo/odoo#52334
Upgrade PR odoo/upgrade#1292
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -531,6 +531,7 @@
|
||||
<field name="state" nolabel="1" colspan="2" widget="statusbar" statusbar_visible="draft,open,done"/>
|
||||
</header>
|
||||
<sheet string="Registration">
|
||||
<div class="oe_button_box" name="button_box"/>
|
||||
<group>
|
||||
<group string="Attendee" name="attendee">
|
||||
<field name="partner_id" attrs="{'readonly':[('state', '!=', 'draft')]}"/>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Event CRM',
|
||||
'version': '1.0',
|
||||
'category': 'Marketing/Events',
|
||||
'website': 'https://www.odoo.com/page/events',
|
||||
'description': "Create leads from event registrations.",
|
||||
'depends': ['event', 'crm'],
|
||||
'data': [
|
||||
'security/event_crm_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/crm_lead_views.xml',
|
||||
'views/event_registration_views.xml',
|
||||
'views/event_lead_rule_views.xml',
|
||||
'views/event_event_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/event_crm_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Event CRM Rule 0 -->
|
||||
<record id="event_lead_rule_0" model="event.lead.rule">
|
||||
<field name="name">Rule on @example.com</field>
|
||||
<field name="event_id" ref="event.event_5"/>
|
||||
<field name="event_registration_filter">[('email','ilike','@example.com')]</field>
|
||||
<field name="lead_user_id" ref="base.user_admin"/>
|
||||
<field name="lead_tag_ids" eval="[(6, 0, [ref('sales_team.categ_oppor8')])]"/>
|
||||
</record>
|
||||
|
||||
<record id="event_registration_0_rule_0" model="event.registration">
|
||||
<field name="name">Barney Lonny</field>
|
||||
<field name="email">barney.lonny@example.com</field>
|
||||
<field name="phone">+1 202 555 0122</field>
|
||||
<field name="event_id" ref="event.event_5"/>
|
||||
</record>
|
||||
<record id="event_registration_1_rule_0" model="event.registration">
|
||||
<field name="name">Tom Harper</field>
|
||||
<field name="email">tom.harper@example.com</field>
|
||||
<field name="phone">+1 202 555 0161</field>
|
||||
<field name="event_id" ref="event.event_5"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import event_lead_rule
|
||||
from . import event_registration
|
||||
from . import crm_lead
|
||||
from . import event_event
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class Lead(models.Model):
|
||||
_inherit = 'crm.lead'
|
||||
|
||||
event_lead_rule_id = fields.Many2one('event.lead.rule', string="Registration Rule", help="Rule that created this lead")
|
||||
event_id = fields.Many2one('event.event', string="Source Event", help="Event triggering the rule that created this lead")
|
||||
registration_ids = fields.Many2many(
|
||||
'event.registration', string="Source Registrations",
|
||||
groups='event.group_event_user',
|
||||
help="Registrations triggering the rule that created this lead")
|
||||
registration_count = fields.Integer(
|
||||
string="# Registrations", compute='_compute_registration_count',
|
||||
groups='event.group_event_user',
|
||||
help="Counter for the registrations linked to this lead")
|
||||
|
||||
@api.depends('registration_ids')
|
||||
def _compute_registration_count(self):
|
||||
for record in self:
|
||||
record.registration_count = len(record.registration_ids)
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class EventEvent(models.Model):
|
||||
_name = "event.event"
|
||||
_inherit = "event.event"
|
||||
|
||||
lead_ids = fields.One2many(
|
||||
'crm.lead', 'event_id', string="Leads", groups='sales_team.group_sale_salesman',
|
||||
help="Leads generated from this event")
|
||||
lead_count = fields.Integer(
|
||||
string="# Leads", compute='_compute_lead_count', groups='sales_team.group_sale_salesman',
|
||||
help="Counter for the leads linked to this event")
|
||||
|
||||
@api.depends('lead_ids')
|
||||
def _compute_lead_count(self):
|
||||
lead_data = self.env['crm.lead'].read_group(
|
||||
[('event_id', 'in', self.ids)],
|
||||
['event_id'], ['event_id']
|
||||
)
|
||||
mapped_data = {item['event_id'][0]: item['event_id_count'] for item in lead_data}
|
||||
for event in self:
|
||||
event.lead_count = mapped_data.get(event.id, 0)
|
||||
@@ -0,0 +1,218 @@
|
||||
# -*- 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))
|
||||
@@ -0,0 +1,334 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class EventRegistration(models.Model):
|
||||
_inherit = 'event.registration'
|
||||
|
||||
lead_ids = fields.Many2many(
|
||||
'crm.lead', string='Leads', copy=False, readonly=True,
|
||||
groups='sales_team.group_sale_salesman',
|
||||
help="Leads generated from the registration.")
|
||||
lead_count = fields.Integer(
|
||||
'# Leads', compute='_compute_lead_count', groups='sales_team.group_sale_salesman',
|
||||
help="Counter for the leads linked to this registration")
|
||||
|
||||
@api.depends('lead_ids')
|
||||
def _compute_lead_count(self):
|
||||
for record in self:
|
||||
record.lead_count = len(record.lead_ids)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" Trigger rules based on registration creation, and check state for
|
||||
rules based on confirmed / done attendees. """
|
||||
registrations = super(EventRegistration, self).create(vals_list)
|
||||
|
||||
# handle triggers based on creation, then those based on confirm and done
|
||||
# as registrations can be automatically confirmed, or even created directly
|
||||
# with a state given in values
|
||||
if not self.env.context.get('event_lead_rule_skip'):
|
||||
self.env['event.lead.rule'].search([('lead_creation_trigger', '=', 'create')]).sudo()._run_on_registrations(registrations)
|
||||
open_registrations = registrations.filtered(lambda reg: reg.state == 'open')
|
||||
if open_registrations:
|
||||
self.env['event.lead.rule'].search([('lead_creation_trigger', '=', 'confirm')]).sudo()._run_on_registrations(open_registrations)
|
||||
done_registrations = registrations.filtered(lambda reg: reg.state == 'done')
|
||||
if done_registrations:
|
||||
self.env['event.lead.rule'].search([('lead_creation_trigger', '=', 'done')]).sudo()._run_on_registrations(done_registrations)
|
||||
|
||||
return registrations
|
||||
|
||||
def write(self, vals):
|
||||
""" Update the lead values depending on fields updated in registrations.
|
||||
There are 2 main use cases
|
||||
|
||||
* first is when we update the partner_id of multiple registrations. It
|
||||
happens when a public user fill its information when he register to
|
||||
an event;
|
||||
* second is when we update specific values of one registration like
|
||||
updating question answers or a contact information (email, phone);
|
||||
|
||||
Also trigger rules based on confirmed and done attendees (state written
|
||||
to open and done).
|
||||
"""
|
||||
to_update, event_lead_rule_skip = False, self.env.context.get('event_lead_rule_skip')
|
||||
if not event_lead_rule_skip:
|
||||
to_update = self.filtered(lambda reg: reg.lead_ids)
|
||||
if to_update:
|
||||
lead_tracked_vals = to_update._get_lead_tracked_values()
|
||||
|
||||
res = super(EventRegistration, self).write(vals)
|
||||
|
||||
if not event_lead_rule_skip and to_update:
|
||||
to_update.flush() # compute notably partner-based fields if necessary
|
||||
to_update.sudo()._update_leads(vals, lead_tracked_vals)
|
||||
|
||||
# handle triggers based on state
|
||||
if not event_lead_rule_skip:
|
||||
if vals.get('state') == 'open':
|
||||
self.env['event.lead.rule'].search([('lead_creation_trigger', '=', 'confirm')]).sudo()._run_on_registrations(self)
|
||||
elif vals.get('state') == 'done':
|
||||
self.env['event.lead.rule'].search([('lead_creation_trigger', '=', 'done')]).sudo()._run_on_registrations(self)
|
||||
|
||||
return res
|
||||
|
||||
def _load_records_create(self, values):
|
||||
""" In import mode: do not run rules those are intended to run when customers
|
||||
buy tickets, not when bootstrapping a database. """
|
||||
return super(EventRegistration, self.with_context(event_lead_rule_skip=True))._load_records_create(values)
|
||||
|
||||
def _load_records_write(self, values):
|
||||
""" In import mode: do not run rules those are intended to run when customers
|
||||
buy tickets, not when bootstrapping a database. """
|
||||
return super(EventRegistration, self.with_context(event_lead_rule_skip=True))._load_records_write(values)
|
||||
|
||||
def _update_leads(self, new_vals, lead_tracked_vals):
|
||||
""" Update leads linked to some registrations. Update is based depending
|
||||
on updated fields, see ``_get_lead_contact_fields()`` and ``_get_lead_
|
||||
description_fields()``. Main heuristic is
|
||||
|
||||
* check attendee-based leads, for each registration recompute contact
|
||||
information if necessary (changing partner triggers the whole contact
|
||||
computation); update description if necessary;
|
||||
* check order-based leads, for each existing group-based lead, only
|
||||
partner change triggers a contact and description update. We consider
|
||||
that group-based rule works mainly with the main contact and less
|
||||
with further details of registrations. Those can be found in stat
|
||||
button if necessary.
|
||||
|
||||
:param new_vals: values given to write. Used to determine updated fields;
|
||||
:param lead_tracked_vals: dict(registration_id, registration previous values)
|
||||
based on new_vals;
|
||||
"""
|
||||
for registration in self:
|
||||
leads_attendee = registration.lead_ids.filtered(
|
||||
lambda lead: lead.event_lead_rule_id.lead_creation_basis == 'attendee'
|
||||
)
|
||||
if not leads_attendee:
|
||||
continue
|
||||
|
||||
old_vals = lead_tracked_vals[registration.id]
|
||||
# if partner has been updated -> update registration contact information
|
||||
# as they are computed (and therefore not given to write values)
|
||||
if 'partner_id' in new_vals:
|
||||
new_vals.update(**dict(
|
||||
(field, registration[field])
|
||||
for field in self._get_lead_contact_fields()
|
||||
if field != 'partner_id')
|
||||
)
|
||||
|
||||
lead_values = {}
|
||||
# update contact fields: valid for all leads of registration
|
||||
upd_contact_fields = [field for field in self._get_lead_contact_fields() if field in new_vals.keys()]
|
||||
if any(new_vals[field] != old_vals[field] for field in upd_contact_fields):
|
||||
lead_values = registration._get_lead_contact_values()
|
||||
|
||||
# update description fields: each lead has to be updated, otherwise
|
||||
# update in batch
|
||||
upd_description_fields = [field for field in self._get_lead_description_fields() if field in new_vals.keys()]
|
||||
if any(new_vals[field] != old_vals[field] for field in upd_description_fields):
|
||||
for lead in leads_attendee:
|
||||
lead_values['description'] = "%s\n%s" % (
|
||||
lead.description,
|
||||
registration._get_lead_description(_("Updated registrations"), line_counter=True)
|
||||
)
|
||||
lead.write(lead_values)
|
||||
elif lead_values:
|
||||
leads_attendee.write(lead_values)
|
||||
|
||||
leads_order = self.lead_ids.filtered(lambda lead: lead.event_lead_rule_id.lead_creation_basis == 'order')
|
||||
for lead in leads_order:
|
||||
lead_values = {}
|
||||
if new_vals.get('partner_id'):
|
||||
lead_values.update(lead.registration_ids._get_lead_contact_values())
|
||||
if not lead.partner_id:
|
||||
lead_values['description'] = lead.registration_ids._get_lead_description(_("Participants"), line_counter=True)
|
||||
elif new_vals['partner_id'] != lead.partner_id.id:
|
||||
lead_values['description'] = lead.description + "\n" + lead.registration_ids._get_lead_description(_("Updated registrations"), line_counter=True, line_suffix=_("(updated)"))
|
||||
if lead_values:
|
||||
lead.write(lead_values)
|
||||
|
||||
def _get_lead_values(self, rule):
|
||||
""" Get lead values from registrations. Self can contain multiple records
|
||||
in which case first found non void value is taken. Note that all
|
||||
registrations should belong to the same event.
|
||||
|
||||
:return dict lead_values: values used for create / write on a lead
|
||||
"""
|
||||
lead_values = {
|
||||
# from rule
|
||||
'type': rule.lead_type,
|
||||
'user_id': rule.lead_user_id.id,
|
||||
'team_id': rule.lead_sales_team_id.id,
|
||||
'tag_ids': rule.lead_tag_ids.ids,
|
||||
'event_lead_rule_id': rule.id,
|
||||
# event and registration
|
||||
'event_id': self.event_id.id,
|
||||
'referred': self.event_id.name,
|
||||
'registration_ids': self.ids,
|
||||
'campaign_id': self._find_first_notnull('utm_campaign_id'),
|
||||
'source_id': self._find_first_notnull('utm_source_id'),
|
||||
'medium_id': self._find_first_notnull('utm_medium_id'),
|
||||
}
|
||||
lead_values.update(self._get_lead_contact_values())
|
||||
lead_values['description'] = self._get_lead_description(_("Participants"), line_counter=True)
|
||||
return lead_values
|
||||
|
||||
def _get_lead_contact_values(self):
|
||||
""" Specific management of contact values. Rule creation basis has some
|
||||
effect on contact management
|
||||
|
||||
* in attendee mode: keep registration partner only if partner phone and
|
||||
email match. Indeed lead are synchronized with their contact and it
|
||||
would imply rewriting on partner, and therefore on other documents;
|
||||
* in batch mode: if a customer is found use it as main contact. Registrations
|
||||
details are included in lead description;
|
||||
|
||||
:return dict: values used for create / write on a lead
|
||||
"""
|
||||
valid_partner = related_partner = next(
|
||||
(reg.partner_id for reg in self if reg.partner_id != self.env.ref('base.public_partner')),
|
||||
self.env['res.partner']
|
||||
) # CHECKME: broader than just public partner
|
||||
|
||||
# mono registration mode: keep partner only if email and phone matches, otherwise registration > partner
|
||||
if len(self) == 1:
|
||||
if (related_partner.phone and self.phone and related_partner.phone != self.phone) or \
|
||||
(related_partner.email and self.email and related_partner.email != self.email):
|
||||
valid_partner = self.env['res.partner']
|
||||
|
||||
if valid_partner:
|
||||
contact_vals = self.env['crm.lead']._prepare_values_from_partner(valid_partner)
|
||||
# force email_from / phone only if not set on partner because those fields are now synchronized automatically
|
||||
if not valid_partner.email:
|
||||
contact_vals['email_from'] = self._find_first_notnull('email')
|
||||
if not valid_partner.phone:
|
||||
contact_vals['email_from'] = self._find_first_notnull('phone')
|
||||
else:
|
||||
# don't force email_from + partner_id because those fields are now synchronized automatically
|
||||
contact_vals = {
|
||||
'contact_name': self._find_first_notnull('name'),
|
||||
'email_from': self._find_first_notnull('email'),
|
||||
'phone': self._find_first_notnull('phone'),
|
||||
}
|
||||
contact_vals.update({
|
||||
'name': "%s - %s" % (self.event_id.name, valid_partner.name or self._find_first_notnull('name') or self._find_first_notnull('email')),
|
||||
'partner_id': valid_partner.id,
|
||||
'mobile': valid_partner.mobile or self._find_first_notnull('mobile'),
|
||||
})
|
||||
return contact_vals
|
||||
|
||||
def _get_lead_description(self, prefix='', line_counter=True, line_suffix=''):
|
||||
""" Build the description for the lead using a prefix for all generated
|
||||
lines. For example to enumerate participants or inform of an update in
|
||||
the information of a participant.
|
||||
|
||||
:return string description: complete description for a lead taking into
|
||||
account all registrations contained in self
|
||||
"""
|
||||
reg_lines = [
|
||||
registration._get_lead_description_registration(
|
||||
prefix="%s. " % (index + 1) if line_counter else "",
|
||||
line_suffix=line_suffix
|
||||
) for index, registration in enumerate(self)
|
||||
]
|
||||
return ("%s\n" % prefix if prefix else "") + ("\n".join(reg_lines))
|
||||
|
||||
def _get_lead_description_registration(self, prefix='', line_suffix=''):
|
||||
""" Build the description line specific to a given registration. """
|
||||
self.ensure_one()
|
||||
return "%s%s (%s)%s" % (
|
||||
prefix or "",
|
||||
self.name or self.partner_id.name or self.email,
|
||||
" - ".join(self[field] for field in ('email', 'phone') if self[field]),
|
||||
" %s" % line_suffix if line_suffix else "",
|
||||
)
|
||||
|
||||
def _get_lead_tracked_values(self):
|
||||
""" Tracked values are based on two subset of fields to track in order
|
||||
to fill or update leads. Two main use cases are
|
||||
|
||||
* description fields: registration contact fields: email, phone, ...
|
||||
on registration. Other fields are added by inheritance like
|
||||
question answers;
|
||||
* contact fields: registration contact fields + partner_id field as
|
||||
contact of a lead is managed specifically. Indeed email and phone
|
||||
synchronization of lead / partner_id implies paying attention to
|
||||
not rewrite partner values from registration values.
|
||||
|
||||
Tracked values are therefore the union of those two field sets. """
|
||||
tracked_fields = list(set(self._get_lead_contact_fields()) or set(self._get_lead_description_fields()))
|
||||
return dict(
|
||||
(registration.id,
|
||||
dict((field, self._convert_value(registration[field], field)) for field in tracked_fields)
|
||||
) for registration in self
|
||||
)
|
||||
|
||||
def _get_lead_grouping(self, rules, rule_to_new_regs):
|
||||
""" Perform grouping of registrations in order to enable order-based
|
||||
lead creation and update existing groups with new registrations.
|
||||
|
||||
Heuristic in event is the following. Registrations created in multi-mode
|
||||
are grouped by event. Customer use case: website_event flow creates
|
||||
several registrations in a create-multi.
|
||||
|
||||
Update is not supported as there is no way to determine if a registration
|
||||
is part of an existing batch.
|
||||
|
||||
:param rules: lead creation rules to run on registrations given by self;
|
||||
:param rule_to_new_regs: dict: for each rule, subset of self matching
|
||||
rule conditions. Used to speedup batch computation;
|
||||
|
||||
:return dict: for each rule, rule (key of dict) gives a list of groups.
|
||||
Each group is a tuple (
|
||||
existing_lead: existing lead to update;
|
||||
group_record: record used to group;
|
||||
registrations: sub record set of self, containing registrations
|
||||
belonging to the same group;
|
||||
)
|
||||
"""
|
||||
event_to_reg_ids = defaultdict(lambda: self.env['event.registration'])
|
||||
for registration in self:
|
||||
event_to_reg_ids[registration.event_id] += registration
|
||||
|
||||
return dict(
|
||||
(rule, [(False, event, (registrations & rule_to_new_regs[rule]).sorted('id'))
|
||||
for event, registrations in event_to_reg_ids.items()])
|
||||
for rule in rules
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# TOOLS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _get_lead_contact_fields(self):
|
||||
""" Get registration fields linked to lead contact. Those are used notably
|
||||
to see if an update of lead is necessary or to fill contact values
|
||||
in ``_get_lead_contact_values())`` """
|
||||
return ['name', 'email', 'phone', 'mobile', 'partner_id']
|
||||
|
||||
@api.model
|
||||
def _get_lead_description_fields(self):
|
||||
""" Get registration fields linked to lead description. Those are used
|
||||
notablyto see if an update of lead is necessary or to fill description
|
||||
in ``_get_lead_description())`` """
|
||||
return ['name', 'email', 'phone']
|
||||
|
||||
def _find_first_notnull(self, field_name):
|
||||
""" Small tool to extract the first not nullvalue of a field: its value
|
||||
or the ids if this is a relational field. """
|
||||
value = next((reg[field_name] for reg in self if reg[field_name]), False)
|
||||
return self._convert_value(value, field_name)
|
||||
|
||||
def _convert_value(self, value, field_name):
|
||||
""" Small tool because convert_to_write is touchy """
|
||||
if value and self._fields[field_name].type in ['many2many', 'one2many']:
|
||||
return value.ids
|
||||
if value and self._fields[field_name].type == 'many2one':
|
||||
return value.id
|
||||
return value
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
<record id="ir_rule_event_crm" model="ir.rule">
|
||||
<field name="name">Event CRM: Multi Company</field>
|
||||
<field name="model_id" ref="model_event_lead_rule"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_multi_company'))]"/>
|
||||
<field name="domain_force">['|', ('company_id','=',False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_event_crm_manager,event.crm.manager,model_event_lead_rule,event.group_event_manager,1,1,1,1
|
||||
access_event_crm_user,event.crm.user,model_event_lead_rule,event.group_event_user,1,0,0,0
|
||||
access_event_crm_salesman,event.crm.user,model_event_lead_rule,sales_team.group_sale_salesman,1,0,0,0
|
||||
|
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_event_crm_flow
|
||||
@@ -0,0 +1,122 @@
|
||||
# -*- 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.addons.event.tests.common import TestEventCommon
|
||||
|
||||
|
||||
class TestEventCrmCommon(TestCrmCommon, TestEventCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestEventCrmCommon, cls).setUpClass()
|
||||
|
||||
# avoid clash with existing rules
|
||||
cls.env['event.lead.rule'].search([]).write({'active': False})
|
||||
|
||||
cls.test_lead_tag = cls.env['crm.tag'].create({'name': 'TagTest'})
|
||||
|
||||
cls.test_rule_attendee = cls.env['event.lead.rule'].create({
|
||||
'name': 'Rule Attendee',
|
||||
'lead_creation_basis': 'attendee',
|
||||
'lead_creation_trigger': 'create',
|
||||
'event_id': cls.event_0.id,
|
||||
'event_registration_filter': [['email', 'ilike', '@test.example.com']],
|
||||
'lead_type': 'lead',
|
||||
'lead_user_id': cls.user_sales_salesman.id,
|
||||
'lead_tag_ids': cls.test_lead_tag,
|
||||
})
|
||||
|
||||
cls.test_rule_order = cls.env['event.lead.rule'].create({
|
||||
'name': 'Rule Order',
|
||||
'lead_creation_basis': 'order',
|
||||
'lead_creation_trigger': 'create',
|
||||
'event_id': cls.event_0.id,
|
||||
'event_registration_filter': [['email', 'ilike', '@test.example.com']],
|
||||
'lead_type': 'opportunity',
|
||||
'lead_user_id': cls.user_sales_leads.id,
|
||||
'lead_sales_team_id': cls.sales_team_1.id,
|
||||
})
|
||||
cls.test_rule_order_done = cls.env['event.lead.rule'].create({
|
||||
'name': 'Rule Order: confirmed partner only',
|
||||
'lead_creation_basis': 'order',
|
||||
'lead_creation_trigger': 'done',
|
||||
'event_registration_filter': [['partner_id', '!=', False]],
|
||||
'lead_type': 'opportunity',
|
||||
})
|
||||
|
||||
cls.batch_customer_data = [{
|
||||
'partner_id': cls.event_customer.id,
|
||||
}] + [{
|
||||
'name': 'My Customer 00',
|
||||
'partner_id': cls.event_customer2.id,
|
||||
'email': 'email.00@test.example.com',
|
||||
'phone': '0456000000',
|
||||
}] + [{
|
||||
'name': 'My Customer %02d' % x,
|
||||
'partner_id': cls.env.ref('base.public_partner').id if x == 0 else False,
|
||||
'email': 'email.%02d@test.example.com' % x,
|
||||
'phone': '04560000%02d' % x,
|
||||
} for x in range(1, 4)]
|
||||
|
||||
def assertLeadConvertion(self, rule, registrations, partner=None, **expected):
|
||||
""" Tool method hiding details of lead value generation and check
|
||||
|
||||
:param lead: lead created through automated rule;
|
||||
:param rule: event.lead.rule that created the lead;
|
||||
:param event: original event;
|
||||
:param registrations: source registrations (singleton or record set if done in batch);
|
||||
:param partner: partner on lead;
|
||||
"""
|
||||
registrations = registrations.sorted('id') # currently order is forced to id ASC
|
||||
lead = self.env['crm.lead'].sudo().search([
|
||||
('registration_ids', 'in', registrations.ids),
|
||||
('event_lead_rule_id', '=', rule.id)
|
||||
])
|
||||
self.assertEqual(len(lead), 1, 'Invalid registrations -> lead creation, found %s leads where only 1 is expected.' % len(lead))
|
||||
self.assertEqual(lead.registration_ids, registrations, 'Invalid registrations -> lead creation, too much registrations on it.')
|
||||
event = registrations.event_id
|
||||
self.assertEqual(len(event), 1, 'Invalid registrations -> event assertion, all registrations should belong to same event')
|
||||
|
||||
if partner is None:
|
||||
partner = self.env['res.partner']
|
||||
expected_reg_name = partner.name or registrations._find_first_notnull('name') or registrations._find_first_notnull('email')
|
||||
if partner:
|
||||
expected_contact_name = partner.name if not partner.is_company else False
|
||||
expected_partner_name = partner.name if partner.is_company else False
|
||||
else:
|
||||
expected_contact_name = registrations._find_first_notnull('name')
|
||||
expected_partner_name = False
|
||||
|
||||
# event information
|
||||
self.assertEqual(lead.event_id, event)
|
||||
self.assertEqual(lead.referred, event.name)
|
||||
|
||||
# registration information
|
||||
self.assertEqual(lead.partner_id, partner)
|
||||
self.assertEqual(lead.name, '%s - %s' % (event.name, expected_reg_name))
|
||||
self.assertNotIn('False', lead.name) # avoid a "Dear False" like construct ^^ (this assert is serious and intended)
|
||||
|
||||
self.assertEqual(lead.contact_name, expected_contact_name)
|
||||
self.assertEqual(lead.partner_name, expected_partner_name)
|
||||
self.assertEqual(lead.email_from, partner.email if partner else registrations._find_first_notnull('email'))
|
||||
self.assertEqual(lead.phone, partner.phone if partner else registrations._find_first_notnull('phone'))
|
||||
self.assertEqual(lead.mobile, partner.mobile if partner and partner.mobile else registrations._find_first_notnull('mobile'))
|
||||
|
||||
# description: to improve
|
||||
self.assertNotIn('False', lead.description) # avoid a "Dear False" like construct ^^ (this assert is serious and intended)
|
||||
for registration in registrations:
|
||||
if registration.name:
|
||||
self.assertIn(registration.name, lead.description)
|
||||
elif registration.partner_id.name:
|
||||
self.assertIn(registration.partner_id.name, lead.description)
|
||||
if registration.email:
|
||||
self.assertIn(registration.email, lead.description)
|
||||
if registration.phone:
|
||||
self.assertIn(registration.phone, lead.description)
|
||||
|
||||
# lead configuration
|
||||
self.assertEqual(lead.type, rule.lead_type)
|
||||
self.assertEqual(lead.user_id, rule.lead_user_id)
|
||||
self.assertEqual(lead.team_id, rule.lead_sales_team_id)
|
||||
self.assertEqual(lead.tag_ids, rule.lead_tag_ids)
|
||||
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.event_crm.tests.common import TestEventCrmCommon
|
||||
from odoo.tests.common import users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestEventCrmFlow(TestEventCrmCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestEventCrmFlow, cls).setUpClass()
|
||||
|
||||
cls.registration_values = [
|
||||
dict(customer_data, event_id=cls.event_0.id)
|
||||
for customer_data in cls.batch_customer_data
|
||||
]
|
||||
cls.registration_values[-1]['email'] = '"John Doe" <invalid@not.example.com>'
|
||||
|
||||
def assert_initial_data(self):
|
||||
self.assertEqual(len(self.registration_values), 5)
|
||||
self.assertEqual(self.event_customer.email_normalized, 'constantin@test.example.com')
|
||||
self.assertEqual(self.event_customer.phone, '0485112233')
|
||||
self.assertFalse(self.event_customer.mobile)
|
||||
|
||||
@users('user_eventmanager')
|
||||
def test_event_crm_flow_batch_create(self):
|
||||
""" Test attendee- and order-based registrations creation. Event-based
|
||||
creation mimics a simplified website_event flow where grouping is done
|
||||
at create. """
|
||||
new_registrations = self.env['event.registration'].create(self.registration_values)
|
||||
self.assertEqual(len(self.event_0.registration_ids), 5)
|
||||
|
||||
# per-attendee rule: one lead for each registration
|
||||
self.assertEqual(len(self.test_rule_attendee.lead_ids), 4)
|
||||
for registration in new_registrations:
|
||||
lead = self.test_rule_attendee.lead_ids.filtered(lambda lead: registration in lead.registration_ids)
|
||||
# test filtering out based on domain
|
||||
if registration.email == '"John Doe" <invalid@not.example.com>':
|
||||
self.assertEqual(lead, self.env['crm.lead'])
|
||||
continue
|
||||
|
||||
# only partner with matching email / phone is kept in mono attendee mode to avoid
|
||||
# loosing registration-specific email / phone informations due to lead synchronization
|
||||
expected_partner = registration.partner_id if registration.partner_id == self.event_customer else None
|
||||
self.assertTrue(bool(lead))
|
||||
self.assertLeadConvertion(self.test_rule_attendee, registration, partner=expected_partner)
|
||||
|
||||
# per-order rule: one lead for all registrations (same event -> same batch, website_event style)
|
||||
self.assertEqual(len(self.test_rule_order.lead_ids), 1)
|
||||
lead = self.test_rule_order.lead_ids
|
||||
self.assertLeadConvertion(
|
||||
self.test_rule_order,
|
||||
new_registrations.filtered(lambda reg: reg.email != '"John Doe" <invalid@not.example.com>'),
|
||||
partner=new_registrations[0].partner_id
|
||||
)
|
||||
# ensuring filtering out worked also at description level
|
||||
self.assertNotIn('invalid@not.example.com', lead.description)
|
||||
|
||||
@users('user_eventmanager')
|
||||
def test_event_crm_flow_batch_update(self):
|
||||
""" Test update of contact or description fields that leads to lead
|
||||
update. """
|
||||
# initial data: create registrations in batch
|
||||
new_registrations = self.env['event.registration'].create(self.registration_values)
|
||||
self.assertEqual(len(self.event_0.registration_ids), 5)
|
||||
self.assertEqual(len(self.test_rule_attendee.lead_ids), 4)
|
||||
self.assertEqual(len(self.test_rule_order.lead_ids), 1)
|
||||
|
||||
# customer is updated (like a SO setting its customer)
|
||||
new_registrations.write({'partner_id': self.event_customer2.id})
|
||||
|
||||
# per-attendee rule
|
||||
self.assertEqual(len(self.test_rule_attendee.lead_ids), 4)
|
||||
for registration in new_registrations:
|
||||
lead = self.test_rule_attendee.lead_ids.filtered(lambda lead: registration in lead.registration_ids)
|
||||
# test filtering out based on domain
|
||||
if registration.email == '"John Doe" <invalid@not.example.com>':
|
||||
self.assertEqual(lead, self.env['crm.lead'])
|
||||
continue
|
||||
|
||||
# only partner with matching email / phone is kept in mono attendee mode to avoid
|
||||
# loosing registration-specific email / phone informations due to lead synchronization
|
||||
self.assertLeadConvertion(self.test_rule_attendee, registration, partner=None)
|
||||
|
||||
# per-order rule
|
||||
self.assertEqual(len(self.test_rule_order.lead_ids), 1)
|
||||
self.assertEqual(self.test_rule_order.lead_ids.event_id, self.event_0)
|
||||
lead = self.test_rule_order.lead_ids
|
||||
self.assertLeadConvertion(
|
||||
self.test_rule_order,
|
||||
new_registrations.filtered(lambda reg: reg.email != '"John Doe" <invalid@not.example.com>'),
|
||||
partner=new_registrations[0].partner_id
|
||||
)
|
||||
# ensuring filtering out worked also at description level
|
||||
self.assertNotIn('invalid@not.example.com', lead.description)
|
||||
|
||||
@users('user_eventmanager')
|
||||
def test_event_crm_flow_per_attendee_single(self):
|
||||
self.assert_initial_data()
|
||||
|
||||
# test: partner-based contact information, everything synchonized
|
||||
registration = self.env['event.registration'].create({
|
||||
'partner_id': self.event_customer.id,
|
||||
'event_id': self.event_0.id,
|
||||
})
|
||||
self.assertEqual(registration.email, self.event_customer.email)
|
||||
self.assertEqual(registration.phone, self.event_customer.phone)
|
||||
|
||||
# per-attendee rule
|
||||
self.assertLeadConvertion(self.test_rule_attendee, registration, partner=registration.partner_id)
|
||||
|
||||
# test: no partner and contact information
|
||||
registration = self.env['event.registration'].create({
|
||||
'name': 'My Registration',
|
||||
'partner_id': False,
|
||||
'email': 'super.email@test.example.com',
|
||||
'phone': False,
|
||||
'mobile': '0456332211',
|
||||
'event_id': self.event_0.id,
|
||||
})
|
||||
self.assertEqual(len(self.event_0.registration_ids), 2)
|
||||
self.assertLeadConvertion(self.test_rule_attendee, registration, partner=None)
|
||||
|
||||
# test: no partner and few information
|
||||
registration = self.env['event.registration'].create({
|
||||
'name': False,
|
||||
'partner_id': False,
|
||||
'email': 'giga.email@test.example.com',
|
||||
'phone': False,
|
||||
'mobile': False,
|
||||
'event_id': self.event_0.id,
|
||||
})
|
||||
self.assertEqual(len(self.event_0.registration_ids), 3)
|
||||
self.assertLeadConvertion(self.test_rule_attendee, registration, partner=None)
|
||||
|
||||
# test: partner but with other contact information -> registration prior
|
||||
registration = self.env['event.registration'].create({
|
||||
'partner_id': self.event_customer.id,
|
||||
'email': 'other.email@test.example.com',
|
||||
'phone': False,
|
||||
'mobile': '0456112233',
|
||||
'event_id': self.event_0.id,
|
||||
})
|
||||
self.assertEqual(len(self.event_0.registration_ids), 4)
|
||||
self.assertLeadConvertion(self.test_rule_attendee, registration, partner=None)
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="event_registration_action_from_lead" model="ir.actions.act_window">
|
||||
<field name="name">Event registrations</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">event.registration</field>
|
||||
<field name="view_mode">tree,kanban,form,calendar,graph</field>
|
||||
<field name="domain">[('lead_ids', '=', active_id)]</field>
|
||||
<field name="context">{'create': False}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No registration found
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="crm_lead_view_form" model="ir.ui.view">
|
||||
<field name="name">crm.lead.view.form.inherit.event.crm</field>
|
||||
<field name="model">crm.lead</field>
|
||||
<field name="inherit_id" ref="crm.crm_lead_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<field name="registration_ids" invisible="1"/>
|
||||
<button name="%(event_registration_action_from_lead)d" type="action" class="oe_stat_button" icon="fa-ticket"
|
||||
attrs="{'invisible': [('registration_count', '=', 0)]}">
|
||||
<div class="o_stat_info">
|
||||
<field name="registration_count"/>
|
||||
<span class="o_stat_text"> Attendees</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="crm_lead_action_from_registration" model="ir.actions.act_window">
|
||||
<field name="name">Leads</field>
|
||||
<field name="res_model">crm.lead</field>
|
||||
<field name="view_mode">tree,kanban,graph,pivot,calendar,form,activity</field>
|
||||
<field name="domain">[('registration_ids', 'in', active_id)]</field>
|
||||
<field name="context">{'create': False}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No leads found
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="crm_lead_action_from_event" model="ir.actions.act_window">
|
||||
<field name="name">Leads</field>
|
||||
<field name="res_model">crm.lead</field>
|
||||
<field name="view_mode">tree,kanban,graph,pivot,calendar,form,activity</field>
|
||||
<field name="domain">[('event_id', '=', active_id)]</field>
|
||||
<field name="context">{'create': False}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No leads found
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="event_view_form" model="ir.ui.view">
|
||||
<field name="name">event.event.form.inherit.event.crm</field>
|
||||
<field name="model">event.event</field>
|
||||
<field name="inherit_id" ref="event.view_event_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='%(event.act_event_registration_from_event)d']" position="after">
|
||||
<button name="%(crm_lead_action_from_event)d" class="oe_stat_button" type="action" icon="fa-star"
|
||||
attrs="{'invisible': [('lead_count', '=' , 0)]}" groups="sales_team.group_sale_salesman">
|
||||
<div class="o_stat_info">
|
||||
<field name="lead_count"/>
|
||||
<span class="o_stat_text"> Leads</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_view_tree" model="ir.ui.view">
|
||||
<field name="name">event.event.tree.inherit.event.crm</field>
|
||||
<field name="model">event.event</field>
|
||||
<field name="inherit_id" ref="event.view_event_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='user_id']" position="after">
|
||||
<field name="lead_count" optional="hide"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="event_lead_rule_view_search" model="ir.ui.view">
|
||||
<field name="name">event.lead.rule.view.search</field>
|
||||
<field name="model">event.lead.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Lead Generation Rules">
|
||||
<field name="name" string="Name"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="filter_inactive" domain="[('active', '=', False)]"/>
|
||||
<filter string="Creation Type" name="filter_lead_creation_basis" context="{'group_by': 'lead_creation_basis'}"/>
|
||||
<filter string="Trigger Type" name="filter_lead_creation_trigger" context="{'group_by': 'lead_creation_trigger'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_lead_rule_view_tree" model="ir.ui.view">
|
||||
<field name="name">event.lead.rule.view.tree</field>
|
||||
<field name="model">event.lead.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Lead Generation Rules">
|
||||
<field name="name"/>
|
||||
<field name="lead_creation_basis" string="Lead Creation Type" invisible="1"/>
|
||||
<field name="lead_creation_trigger"/>
|
||||
<field name="event_type_ids" widget="many2many_tags"/>
|
||||
<field name="event_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_lead_rule_view_form" model="ir.ui.view">
|
||||
<field name="name">event.lead.rule.view.form</field>
|
||||
<field name="model">event.lead.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Lead Generation Rule">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name" placeholder="e.g. B2B Fairs"/></h1>
|
||||
</div>
|
||||
<field name="active" invisible="1"/>
|
||||
<group name="lead_creation_configuration">
|
||||
<group name="lead_creation_basis" invisible="1">
|
||||
<field name="lead_creation_basis" widget="radio"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="lead_creation_trigger" widget="radio"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="For any of these Events">
|
||||
<group>
|
||||
<field name="event_type_ids" widget="many2many_tags"/>
|
||||
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="event_id" options="{'no_create': True}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="If the Attendees meet these Conditions">
|
||||
<field name="event_registration_filter" widget="domain" options="{'model': 'event.registration'}" nolabel="1"/>
|
||||
</group>
|
||||
<group string="Lead Default Values">
|
||||
<group class="col">
|
||||
<field name="lead_type" groups="crm.group_use_lead"/>
|
||||
<field name="lead_sales_team_id"/>
|
||||
<field name="lead_user_id"/>
|
||||
</group>
|
||||
<group class="col">
|
||||
<field name="lead_tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="event_lead_rule_action" model="ir.actions.act_window">
|
||||
<field name="name">Lead Generation Rule</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">event.lead.rule</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">Create a Lead Generation Rule</p>
|
||||
<p>Those automatically create leads when attendees register.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Lead Generation"
|
||||
id="event_lead_rule_menu"
|
||||
action="event_lead_rule_action"
|
||||
parent="event.menu_event_configuration"
|
||||
sequence="10"
|
||||
groups="event.group_event_manager"/>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="event_registration_view_form" model="ir.ui.view">
|
||||
<field name="name">event.registration.form.inherit.event.crm</field>
|
||||
<field name="model">event.registration</field>
|
||||
<field name="inherit_id" ref="event.view_event_registration_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="%(crm_lead_action_from_registration)d" class="oe_stat_button" type="action" icon="fa-star"
|
||||
attrs="{'invisible': [('lead_count', '=' , 0)]}" groups="sales_team.group_sale_salesman">
|
||||
<div class="o_stat_info">
|
||||
<field name="lead_count"/>
|
||||
<span class="o_stat_text"> Leads</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Event CRM Sale',
|
||||
'version': '1.0',
|
||||
'category': 'Marketing/Events',
|
||||
'website': 'https://www.odoo.com/page/events',
|
||||
'description': "Add information of sale order linked to the registration for the creation of the lead.",
|
||||
'depends': ['event_crm', 'event_sale'],
|
||||
'data': [
|
||||
'views/event_lead_rule_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import event_registration
|
||||
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class EventRegistration(models.Model):
|
||||
_inherit = 'event.registration'
|
||||
|
||||
def _get_lead_grouping(self, rules, rule_to_new_regs):
|
||||
""" Override to support sale-order based grouping and update.
|
||||
|
||||
When checking for groups for rules, we search for existing leads linked
|
||||
to same group (based on sale_order_id) and rule. Each rule can therefore
|
||||
update an existing lead or create a new one, for each sale order that
|
||||
makes the group. """
|
||||
so_registrations = self.filtered(lambda reg: reg.sale_order_id)
|
||||
grouping_res = super(EventRegistration, self - so_registrations)._get_lead_grouping(rules, rule_to_new_regs)
|
||||
|
||||
if so_registrations:
|
||||
# find existing leads in batch to put them in cache and avoid multiple search / queries
|
||||
related_registrations = self.env['event.registration'].search([
|
||||
('sale_order_id', 'in', so_registrations.sale_order_id.ids)
|
||||
])
|
||||
related_leads = self.env['crm.lead'].search([
|
||||
('event_lead_rule_id', 'in', rules.ids),
|
||||
('registration_ids', 'in', related_registrations.ids)
|
||||
])
|
||||
|
||||
for rule in rules:
|
||||
rule_new_regs = rule_to_new_regs[rule]
|
||||
|
||||
# for each group (sale_order), find its linked registrations
|
||||
so_to_regs = defaultdict(lambda: self.env['event.registration'])
|
||||
for registration in rule_new_regs & so_registrations:
|
||||
so_to_regs[registration.sale_order_id] |= registration
|
||||
|
||||
# for each grouped registrations, prepare result with group and existing lead
|
||||
so_res = []
|
||||
for sale_order, registrations in so_to_regs.items():
|
||||
registrations = registrations.sorted('id') # as an OR was used, re-ensure order
|
||||
leads = related_leads.filtered(lambda lead: lead.event_lead_rule_id == rule and lead.registration_ids.sale_order_id == sale_order)
|
||||
so_res.append((leads, sale_order, registrations))
|
||||
if so_res:
|
||||
grouping_res[rule] = grouping_res.get(rule, list()) + so_res
|
||||
|
||||
return grouping_res
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="event_lead_rule_view_tree" model="ir.ui.view">
|
||||
<field name="name">event.lead.rule.view.tree.inherit.event.crm.sale</field>
|
||||
<field name="model">event.lead.rule</field>
|
||||
<field name="inherit_id" ref="event_crm.event_lead_rule_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='lead_creation_basis']" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="event_lead_rule_view_form" model="ir.ui.view">
|
||||
<field name="name">event.lead.rule.view.form.inherit.event.crm.sale</field>
|
||||
<field name="model">event.lead.rule</field>
|
||||
<field name="inherit_id" ref="event_crm.event_lead_rule_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='lead_creation_basis']" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{
|
||||
'name': 'Events Sales',
|
||||
'version': '1.1',
|
||||
'version': '1.2',
|
||||
'category': 'Marketing/Events',
|
||||
'website': 'https://www.odoo.com/page/events',
|
||||
'description': """
|
||||
|
||||
@@ -10,8 +10,8 @@ class EventRegistration(models.Model):
|
||||
|
||||
is_paid = fields.Boolean('Is Paid')
|
||||
# TDE FIXME: maybe add an onchange on sale_order_id
|
||||
sale_order_id = fields.Many2one('sale.order', string='Source Sales Order', ondelete='cascade')
|
||||
sale_order_line_id = fields.Many2one('sale.order.line', string='Sales Order Line', ondelete='cascade')
|
||||
sale_order_id = fields.Many2one('sale.order', string='Source Sales Order', ondelete='cascade', copy=False)
|
||||
sale_order_line_id = fields.Many2one('sale.order.line', string='Sales Order Line', ondelete='cascade', copy=False)
|
||||
payment_status = fields.Selection(string="Payment Status", selection=[
|
||||
('to_pay', 'Not Paid'),
|
||||
('paid', 'Paid'),
|
||||
|
||||
@@ -71,8 +71,9 @@ class SaleOrderLine(models.Model):
|
||||
order line has a product_uom_qty attribute that will be the number of
|
||||
registrations linked to this line. This method update existing registrations
|
||||
and create new one for missing one. """
|
||||
Registration = self.env['event.registration'].sudo()
|
||||
registrations = Registration.search([('sale_order_line_id', 'in', self.ids)])
|
||||
RegistrationSudo = self.env['event.registration'].sudo()
|
||||
registrations = RegistrationSudo.search([('sale_order_line_id', 'in', self.ids)])
|
||||
registrations_vals = []
|
||||
for so_line in self.filtered('event_id'):
|
||||
existing_registrations = registrations.filtered(lambda self: self.sale_order_line_id.id == so_line.id)
|
||||
if confirm:
|
||||
@@ -82,7 +83,6 @@ class SaleOrderLine(models.Model):
|
||||
if cancel_to_draft:
|
||||
existing_registrations.filtered(lambda self: self.state == 'cancel').action_set_draft()
|
||||
|
||||
registrations_vals = []
|
||||
for count in range(int(so_line.product_uom_qty) - len(existing_registrations)):
|
||||
values = {
|
||||
'sale_order_line_id': so_line.id,
|
||||
@@ -92,7 +92,9 @@ class SaleOrderLine(models.Model):
|
||||
if registration_data:
|
||||
values.update(registration_data.pop())
|
||||
registrations_vals.append(values)
|
||||
Registration.create(registrations_vals)
|
||||
|
||||
if registrations_vals:
|
||||
RegistrationSudo.create(registrations_vals)
|
||||
return True
|
||||
|
||||
@api.onchange('product_id')
|
||||
|
||||
@@ -30,22 +30,22 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_event_registration_ticket_form" model="ir.ui.view">
|
||||
<record id="event_registration_ticket_view_form" model="ir.ui.view">
|
||||
<field name="name">event.registration.form.inherit</field>
|
||||
<field name="model">event.registration</field>
|
||||
<field name="inherit_id" ref="event.view_event_registration_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_view_sale_order" type="object"
|
||||
class="oe_stat_button" icon="fa-usd" string="Sale Order">
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//group" position="before">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_sale_order" type="object"
|
||||
class="oe_stat_button" icon="fa-usd" string="Sale Order">
|
||||
</button>
|
||||
</div>
|
||||
<field name="is_paid" invisible="1"/>
|
||||
<widget name="web_ribbon" title="Paid" bg_color="bg-success" attrs="{'invisible': [('is_paid', '=', False)]}"/>
|
||||
</xpath>
|
||||
<group name="utm_link" position="before">
|
||||
<group string="Origin" groups="base.group_no_one">
|
||||
<group string="Transaction" groups="base.group_no_one">
|
||||
<field name="sale_order_id"/>
|
||||
<field name="sale_order_line_id" readonly="1" attrs="{'invisible': [('sale_order_id', '=', False)]}"/>
|
||||
</group>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
</tree>
|
||||
</field>
|
||||
<footer>
|
||||
<button string="Apply" name="action_make_registration" type="object" class="btn-primary"/>
|
||||
<button string="Skip" class="btn-secondary" special="cancel"/>
|
||||
<button string="Confirm" name="action_make_registration" type="object" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import tests
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Test Full Event Flow',
|
||||
'version': '1.0',
|
||||
'category': 'Hidden/Tests',
|
||||
'description': """
|
||||
This module will test the main event flows of Odoo, both frontend and backend.
|
||||
It installs sale capabilities, front-end flow, eCommerce, questions and
|
||||
automatic lead generation..
|
||||
""",
|
||||
'depends': [
|
||||
'event',
|
||||
'event_crm',
|
||||
'event_sale',
|
||||
'website_event_crm_questions',
|
||||
'website_event_questions',
|
||||
'website_event_sale',
|
||||
'website_event_track',
|
||||
],
|
||||
'demo': [],
|
||||
'data': [],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from . import test_event_crm
|
||||
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.event_crm.tests.common import TestEventCrmCommon
|
||||
|
||||
|
||||
class TestEventFullCommon(TestEventCrmCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestEventFullCommon, cls).setUpClass()
|
||||
|
||||
cls.event_product = cls.env['product.product'].create({
|
||||
'name': 'Test Registration Product',
|
||||
'description_sale': 'Mighty Description',
|
||||
'list_price': 10,
|
||||
'event_ok': True,
|
||||
'standard_price': 30.0,
|
||||
'type': 'service',
|
||||
})
|
||||
|
||||
cls.event_0.write({
|
||||
'event_ticket_ids': [
|
||||
(5, 0),
|
||||
(0, 0, {
|
||||
'name': 'First Ticket',
|
||||
'product_id': cls.event_product.id,
|
||||
'seats_max': 30,
|
||||
}), (0, 0, {
|
||||
'name': 'Second Ticket',
|
||||
'product_id': cls.event_product.id,
|
||||
})
|
||||
],
|
||||
})
|
||||
|
||||
cls.event_question_1 = cls.env['event.question'].create({
|
||||
'title': 'Question1',
|
||||
'question_type': 'simple_choice',
|
||||
'event_id': cls.event_0.id,
|
||||
'once_per_order': False,
|
||||
'answer_ids': [
|
||||
(0, 0, {'name': 'Q1-Answer1'}),
|
||||
(0, 0, {'name': 'Q1-Answer2'})
|
||||
],
|
||||
})
|
||||
cls.event_question_2 = cls.env['event.question'].create({
|
||||
'title': 'Question2',
|
||||
'question_type': 'simple_choice',
|
||||
'event_id': cls.event_0.id,
|
||||
'once_per_order': True,
|
||||
'answer_ids': [
|
||||
(0, 0, {'name': 'Q2-Answer1'}),
|
||||
(0, 0, {'name': 'Q2-Answer2'})
|
||||
],
|
||||
})
|
||||
cls.event_question_3 = cls.env['event.question'].create({
|
||||
'title': 'Question3',
|
||||
'question_type': 'text_box',
|
||||
'event_id': cls.event_0.id,
|
||||
'once_per_order': True,
|
||||
})
|
||||
|
||||
# make a SO for a customer, selling some tickets
|
||||
cls.customer_so = cls.env['sale.order'].with_user(cls.user_sales_salesman).create({
|
||||
'partner_id': cls.event_customer.id,
|
||||
})
|
||||
|
||||
cls.website_customer_data = [{
|
||||
'name': 'My Customer %02d' % x,
|
||||
'partner_id': cls.env.ref('base.public_partner').id,
|
||||
'email': 'email.%02d@test.example.com' % x,
|
||||
'phone': '04560000%02d' % x,
|
||||
'registration_answer_ids': [
|
||||
(0, 0, {
|
||||
'question_id': cls.event_question_1.id,
|
||||
'value_answer_id': cls.event_question_1.answer_ids[(x % 2)].id,
|
||||
}), (0, 0, {
|
||||
'question_id': cls.event_question_2.id,
|
||||
'value_answer_id': cls.event_question_2.answer_ids[(x % 2)].id,
|
||||
}), (0, 0, {
|
||||
'question_id': cls.event_question_3.id,
|
||||
'value_text_box': 'CustomerAnswer%s' % x,
|
||||
})
|
||||
],
|
||||
} for x in range(0, 4)]
|
||||
|
||||
def assertLeadConvertion(self, rule, registrations, partner=None, **expected):
|
||||
super(TestEventFullCommon, self).assertLeadConvertion(rule, registrations, partner=partner, **expected)
|
||||
lead = self.env['crm.lead'].sudo().search([
|
||||
('registration_ids', 'in', registrations.ids),
|
||||
('event_lead_rule_id', '=', rule.id)
|
||||
])
|
||||
|
||||
for registration in registrations:
|
||||
if not registration.registration_answer_ids:
|
||||
continue
|
||||
for answer in registration.registration_answer_ids:
|
||||
self.assertIn(answer.question_id.title, lead.description)
|
||||
if answer.question_type == 'simple_choice':
|
||||
self.assertIn(answer.value_answer_id.name, lead.description)
|
||||
else:
|
||||
self.assertIn(answer.value_text_box, lead.description) # better: check multi line
|
||||
@@ -0,0 +1,149 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.test_event_full.tests.common import TestEventFullCommon
|
||||
from odoo.tests import users
|
||||
|
||||
|
||||
class TestEventCrm(TestEventFullCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestEventCrm, cls).setUpClass()
|
||||
|
||||
cls.TICKET1_COUNT, cls.TICKET2_COUNT = 3, 1
|
||||
ticket1 = cls.event_0.event_ticket_ids[0]
|
||||
ticket2 = cls.event_0.event_ticket_ids[1]
|
||||
|
||||
# PREPARE SO DATA
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# adding some tickets to SO
|
||||
cls.customer_so.write({
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'event_id': cls.event_0.id,
|
||||
'event_ticket_id': ticket1.id,
|
||||
'product_id': ticket1.product_id.id,
|
||||
'product_uom_qty': cls.TICKET1_COUNT,
|
||||
}), (0, 0, {
|
||||
'event_id': cls.event_0.id,
|
||||
'event_ticket_id': ticket2.id,
|
||||
'product_id': ticket2.product_id.id,
|
||||
'product_uom_qty': cls.TICKET2_COUNT,
|
||||
'price_unit': 50,
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
@users('user_sales_salesman')
|
||||
def test_event_crm_sale_customer(self):
|
||||
""" Test a SO with a real customer set on it, check partner propagation
|
||||
as well as group-based lead update. """
|
||||
customer_so = self.env['sale.order'].browse(self.customer_so.id)
|
||||
|
||||
# adding some tickets to SO
|
||||
t1_reg_vals = [
|
||||
dict(customer_data,
|
||||
partner_id=customer_so.partner_id.id,
|
||||
sale_order_line_id=customer_so.order_line[0].id)
|
||||
for customer_data in self.website_customer_data[:self.TICKET1_COUNT]
|
||||
]
|
||||
t1_registrations = self.env['event.registration'].create(t1_reg_vals)
|
||||
|
||||
# check effect: registrations, leads
|
||||
self.assertEqual(self.event_0.registration_ids, t1_registrations)
|
||||
self.assertEqual(len(self.test_rule_order.lead_ids), 1)
|
||||
self.assertEqual(self.test_rule_order_done.lead_ids, self.env['crm.lead'])
|
||||
# check lead converted based on registrations
|
||||
self.assertLeadConvertion(self.test_rule_order, t1_registrations, partner=customer_so.partner_id)
|
||||
|
||||
# SO is confirmed -> missing registrations should be automatically added
|
||||
# and added to the lead as part of the same group
|
||||
customer_so.action_confirm()
|
||||
self.assertEqual(customer_so.state, 'sale')
|
||||
self.assertEqual(len(self.event_0.registration_ids), self.TICKET1_COUNT + self.TICKET2_COUNT)
|
||||
self.assertEqual(len(self.test_rule_order.lead_ids), 1) # no new lead created
|
||||
self.assertEqual(self.test_rule_order_done.lead_ids, self.env['crm.lead']) # this one still not triggered
|
||||
|
||||
# check existing lead has been updated with new registrations
|
||||
self.assertLeadConvertion(self.test_rule_order, self.event_0.registration_ids, partner=customer_so.partner_id)
|
||||
|
||||
# Confirm registrations -> trigger the "DONE" rule, one new lead linked to all
|
||||
# event registrations created in this test as all belong to the same SO
|
||||
self.event_0.registration_ids.write({'state': 'done'})
|
||||
self.assertLeadConvertion(self.test_rule_order_done, self.event_0.registration_ids, partner=customer_so.partner_id)
|
||||
|
||||
@users('user_sales_salesman')
|
||||
def test_event_crm_sale_mixed_group(self):
|
||||
""" Test a mixed sale order line creation. This should not happen in a customer
|
||||
use case but should be supported by the code. """
|
||||
public_partner = self.env.ref('base.public_partner')
|
||||
public_so = self.env['sale.order'].create({
|
||||
'partner_id': public_partner.id,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'event_id': self.event_0.id,
|
||||
'event_ticket_id': self.event_0.event_ticket_ids[0].id,
|
||||
'product_id': self.event_0.event_ticket_ids[0].product_id.id,
|
||||
'product_uom_qty': 2,
|
||||
})
|
||||
]
|
||||
})
|
||||
customer_so = self.env['sale.order'].browse(self.customer_so.id)
|
||||
|
||||
# make a multi-SO create
|
||||
mixed_reg_vals = [
|
||||
dict(self.website_customer_data[0],
|
||||
partner_id=customer_so.partner_id.id,
|
||||
sale_order_line_id=customer_so.order_line[0].id),
|
||||
dict(self.website_customer_data[1],
|
||||
partner_id=customer_so.partner_id.id,
|
||||
sale_order_line_id=customer_so.order_line[0].id),
|
||||
dict(self.website_customer_data[2],
|
||||
partner_id=public_so.partner_id.id,
|
||||
sale_order_line_id=public_so.order_line[0].id),
|
||||
dict(self.website_customer_data[3],
|
||||
partner_id=public_so.partner_id.id,
|
||||
sale_order_line_id=public_so.order_line[0].id),
|
||||
]
|
||||
self.env['event.registration'].create(mixed_reg_vals)
|
||||
|
||||
public_regs = self.event_0.registration_ids.filtered(lambda reg: reg.sale_order_id == public_so)
|
||||
self.assertEqual(len(public_regs), 2)
|
||||
customer_regs = self.event_0.registration_ids.filtered(lambda reg: reg.sale_order_id == customer_so)
|
||||
self.assertEqual(len(customer_regs), 2)
|
||||
self.assertLeadConvertion(self.test_rule_order, public_regs, partner=None)
|
||||
self.assertLeadConvertion(self.test_rule_order, customer_regs, partner=customer_so.partner_id)
|
||||
|
||||
@users('user_sales_salesman')
|
||||
def test_event_crm_sale_public(self):
|
||||
""" Test a SO with a public partner on it, then updated when SO is confirmed.
|
||||
This somehow simulates a simplified website_event_sale flow. """
|
||||
public_partner = self.env.ref('base.public_partner')
|
||||
customer_so = self.env['sale.order'].browse(self.customer_so.id)
|
||||
customer_so.write({'partner_id': public_partner.id})
|
||||
|
||||
# adding some tickets to SO
|
||||
t1_reg_vals = [
|
||||
dict(customer_data,
|
||||
partner_id=public_partner.id,
|
||||
sale_order_line_id=customer_so.order_line[0].id)
|
||||
for customer_data in self.website_customer_data[:self.TICKET1_COUNT]
|
||||
]
|
||||
t1_registrations = self.env['event.registration'].create(t1_reg_vals)
|
||||
self.assertEqual(self.event_0.registration_ids, t1_registrations)
|
||||
|
||||
# check lead converted based on registrations
|
||||
self.assertLeadConvertion(self.test_rule_order, t1_registrations, partner=None)
|
||||
|
||||
# SO is confirmed -> missing registrations should be automatically added
|
||||
# BUT as public user -> no email -> not taken into account by rule
|
||||
customer_so.action_confirm()
|
||||
self.assertEqual(customer_so.state, 'sale')
|
||||
self.assertEqual(len(self.event_0.registration_ids), self.TICKET1_COUNT + self.TICKET2_COUNT)
|
||||
self.assertLeadConvertion(self.test_rule_order, t1_registrations, partner=None)
|
||||
|
||||
# SO has a customer set -> main contact of lead is updated accordingly
|
||||
customer_so.write({'partner_id': self.event_customer.id})
|
||||
self.assertLeadConvertion(self.test_rule_order, t1_registrations, partner=self.event_customer)
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Website Events CRM',
|
||||
'version': '1.0',
|
||||
'category': 'Website/Website',
|
||||
'website': 'https://www.odoo.com/page/events',
|
||||
'description': "Allow per-order lead creation mode",
|
||||
'depends': ['event_crm', 'website_event'],
|
||||
'data': [
|
||||
'views/event_lead_rule_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/event_crm_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Event CRM Rule 1 -->
|
||||
<record id="event_lead_rule_1" model="event.lead.rule">
|
||||
<field name="name">Rule per order</field>
|
||||
<field name="lead_creation_basis">order</field>
|
||||
<field name="event_id" ref="event.event_0"/>
|
||||
<field name="lead_user_id" ref="base.user_demo"/>
|
||||
<field name="lead_tag_ids" eval="[(6, 0, [ref('sales_team.categ_oppor3')]),(6, 0, [ref('sales_team.categ_oppor6')])]"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="event_lead_rule_view_tree" model="ir.ui.view">
|
||||
<field name="name">event.lead.rule.view.tree.inherit.website.event.crm</field>
|
||||
<field name="model">event.lead.rule</field>
|
||||
<field name="inherit_id" ref="event_crm.event_lead_rule_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='lead_creation_basis']" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="event_lead_rule_view_form" model="ir.ui.view">
|
||||
<field name="name">event.lead.rule.view.form.inherit.website.event.crm</field>
|
||||
<field name="model">event.lead.rule</field>
|
||||
<field name="inherit_id" ref="event_crm.event_lead_rule_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='lead_creation_basis']" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Webiste Event CRM Questions',
|
||||
'version': '1.0',
|
||||
'category': 'Marketing/Events',
|
||||
'website': 'https://www.odoo.com/page/events',
|
||||
'description': """
|
||||
Add information when we build the description of a lead to include
|
||||
the questions and answers linked to the registrations.
|
||||
""",
|
||||
'depends': ['website_event_crm', 'website_event_questions'],
|
||||
'data': [],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import event_registration
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, _
|
||||
|
||||
|
||||
class EventRegistration(models.Model):
|
||||
_inherit = 'event.registration'
|
||||
|
||||
def _get_lead_description_registration(self, prefix='', line_suffix=''):
|
||||
"""Add the questions and answers linked to the registrations into the description of the lead."""
|
||||
reg_description = super(EventRegistration, self)._get_lead_description_registration(prefix=prefix, line_suffix=line_suffix)
|
||||
if not self.registration_answer_ids:
|
||||
return reg_description
|
||||
|
||||
answer_descriptions = []
|
||||
for answer in self.registration_answer_ids:
|
||||
answer_value = answer.value_answer_id.name if answer.question_type == "simple_choice" else answer.value_text_box
|
||||
answer_value = "\n".join([" %s" % line for line in answer_value.split('\n')])
|
||||
answer_descriptions.append(" - %s\n%s" % (answer.question_id.title, answer_value))
|
||||
return "%s\n%s\n%s" % (reg_description, _("Questions"), '\n'.join(answer_descriptions))
|
||||
|
||||
def _get_lead_description_fields(self):
|
||||
res = super(EventRegistration, self)._get_lead_description_fields()
|
||||
res.append('registration_answer_ids')
|
||||
return res
|
||||
Reference in New Issue
Block a user