359 lines
18 KiB
Python
359 lines
18 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from collections import defaultdict
|
||
|
from markupsafe import Markup
|
||
|
|
||
|
from odoo import api, fields, models, tools, _
|
||
|
from odoo.addons.phone_validation.tools import phone_validation
|
||
|
|
||
|
|
||
|
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')
|
||
|
lead_count = fields.Integer(
|
||
|
'# Leads', compute='_compute_lead_count', compute_sudo=True)
|
||
|
|
||
|
@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 they 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_count)
|
||
|
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:
|
||
|
self.env.flush_all() # 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<br/>%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 + "<br/>" + 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 = 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. Note that email format and phone
|
||
|
# formatting have to taken into account in comparison
|
||
|
if len(self) == 1 and valid_partner:
|
||
|
# compare emails: email_normalized or raw
|
||
|
if self.email and valid_partner.email:
|
||
|
if valid_partner.email_normalized and tools.email_normalize(self.email) != valid_partner.email_normalized:
|
||
|
valid_partner = self.env['res.partner']
|
||
|
elif not valid_partner.email_normalized and valid_partner.email != self.email:
|
||
|
valid_partner = self.env['res.partner']
|
||
|
|
||
|
# compare phone, taking into account formatting
|
||
|
if valid_partner and self.phone and valid_partner.phone:
|
||
|
phone_formatted = self._phone_format(fname='phone', country=valid_partner.country_id)
|
||
|
partner_phone_formatted = valid_partner._phone_format(fname='phone')
|
||
|
if phone_formatted and partner_phone_formatted and phone_formatted != partner_phone_formatted:
|
||
|
valid_partner = self.env['res.partner']
|
||
|
if (not phone_formatted or not partner_phone_formatted) and self.phone != valid_partner.phone:
|
||
|
valid_partner = self.env['res.partner']
|
||
|
|
||
|
registration_phone = self._find_first_notnull('phone')
|
||
|
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['phone'] = registration_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': registration_phone,
|
||
|
'lang_id': False,
|
||
|
}
|
||
|
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,
|
||
|
})
|
||
|
# try to avoid copying registration_phone on both phone and mobile fields
|
||
|
# as would be noise; pay attention partner.hone is propagated through compute
|
||
|
mobile = valid_partner.mobile or registration_phone
|
||
|
if mobile != contact_vals.get('phone', valid_partner.phone):
|
||
|
contact_vals['mobile'] = valid_partner.mobile or registration_phone
|
||
|
|
||
|
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(
|
||
|
line_suffix=line_suffix
|
||
|
) for registration in self
|
||
|
]
|
||
|
description = (prefix if prefix else '') + Markup("<br/>")
|
||
|
if line_counter:
|
||
|
description += Markup("<ol>") + Markup('').join(reg_lines) + Markup("</ol>")
|
||
|
else:
|
||
|
description += Markup("<ul>") + Markup('').join(reg_lines) + Markup("</ul>")
|
||
|
return description
|
||
|
|
||
|
def _get_lead_description_registration(self, line_suffix=''):
|
||
|
""" Build the description line specific to a given registration. """
|
||
|
self.ensure_one()
|
||
|
return Markup("<li>") + "%s (%s)%s" % (
|
||
|
self.name or self.partner_id.name or self.email,
|
||
|
" - ".join(self[field] for field in ('email', 'phone') if self[field]),
|
||
|
f" {line_suffix}" if line_suffix else "",
|
||
|
) + Markup("</li>")
|
||
|
|
||
|
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', 'partner_id']
|
||
|
|
||
|
@api.model
|
||
|
def _get_lead_description_fields(self):
|
||
|
""" Get registration fields linked to lead description. Those are used
|
||
|
notably to 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 isinstance(value, models.BaseModel) and self._fields[field_name].type in ['many2many', 'one2many']:
|
||
|
return value.ids
|
||
|
if isinstance(value, models.BaseModel) and self._fields[field_name].type == 'many2one':
|
||
|
return value.id
|
||
|
return value
|