1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
|
# -*- 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'):
registrations._apply_lead_generation_rules()
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 _apply_lead_generation_rules(self):
leads = self.env['crm.lead']
open_registrations = self.filtered(lambda reg: reg.state == 'open')
done_registrations = self.filtered(lambda reg: reg.state == 'done')
leads += self.env['event.lead.rule'].search(
[('lead_creation_trigger', '=', 'create')]
).sudo()._run_on_registrations(self)
if open_registrations:
leads += self.env['event.lead.rule'].search(
[('lead_creation_trigger', '=', 'confirm')]
).sudo()._run_on_registrations(open_registrations)
if done_registrations:
leads += self.env['event.lead.rule'].search(
[('lead_creation_trigger', '=', 'done')]
).sudo()._run_on_registrations(done_registrations)
return leads
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
"""
sorted_self = self.sorted("id")
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': sorted_self._find_first_notnull('utm_campaign_id'),
'source_id': sorted_self._find_first_notnull('utm_source_id'),
'medium_id': sorted_self._find_first_notnull('utm_medium_id'),
}
lead_values.update(sorted_self._get_lead_contact_values())
lead_values['description'] = sorted_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
"""
sorted_self = self.sorted("id")
valid_partner = next(
(reg.partner_id for reg in sorted_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 = sorted_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'] = sorted_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': sorted_self._find_first_notnull('name'),
'email_from': sorted_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 sorted_self._find_first_notnull('name') or sorted_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()) | 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 and creation_date. Customer use case: website_event
flow creates several registrations in a create-multi. Cron use case:
when running a rule on existing registrations, grouping on event only
is not sufficient, create_date is a safe bet for registration groups.
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;
)
"""
grouped_registrations = {
(create_date, event): sub_registrations
for event, registrations in self.grouped('event_id').items()
for create_date, sub_registrations in registrations.grouped('create_date').items()
}
return dict(
(rule, [(False, key, (registrations & rule_to_new_regs[rule]).sorted('id'))
for key, registrations in grouped_registrations.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
|