[IMP] event: format mobile / phone number of attendees when possible

ISSUE

SMS reminders are not always correctly sent, as people fill form using local
numbers. When scheduler run the number is not always recognized to be
formatted using E164 format. We should try to normalize numbers when possible.

SPECIFICATIONS

Try a sanitize: format number based on event country (or partner if set).
If formatting it works, use the given number, otherwise use the original
number.

Do it at registration creation, as classic flow is the frontend where customers
give numbers used to create registrations. When the number is coming from the
partner through the compute, do not do anything and keep current behavior
which copies partner number if not given directly on the registration.

Add an onchange to try to format the number in the form view, like what is
done on leads. Do not automatically do it at write as it is not really
required currently from business point of view, it is more an helper for
frontend registration.

Task-3342820

closes odoo/odoo#128118

X-original-commit: 94fa8d962535a754c9fb26997a38cc8b51b8974e
Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Thibault Delavallée
2023-07-12 12:38:48 +02:00
parent 0f26b490e1
commit e2711f46ff
4 changed files with 203 additions and 11 deletions
+54
View File
@@ -7,6 +7,15 @@ from odoo import _, api, fields, models, SUPERUSER_ID
from odoo.tools import format_date
from odoo.exceptions import AccessError, ValidationError
# phone_validation is not officially in the depends of event, but we would like
# to have the formatting available in event, not in event_sms -> do a conditional
# import just to be sure
try:
from odoo.addons.phone_validation.tools.phone_validation import phone_format
except ImportError:
def phone_format(number, country_code, country_phone_code, force_format='INTERNATIONAL', raise_exception=True):
return number
class EventRegistration(models.Model):
_name = 'event.registration'
@@ -121,12 +130,43 @@ class EventRegistration(models.Model):
return dict((fname, contact[fname]) for fname in fnames if contact[fname])
return {}
@api.onchange('phone', 'event_id', 'partner_id')
def _onchange_phone_validation(self):
if self.phone:
country = self.partner_id.country_id or self.event_id.country_id or self.env.company.country_id
self.phone = self._phone_format(self.phone, country)
@api.onchange('mobile', 'event_id', 'partner_id')
def _onchange_mobile_validation(self):
if self.mobile:
country = self.partner_id.country_id or self.event_id.country_id or self.env.company.country_id
self.mobile = self._phone_format(self.mobile, country)
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
# format numbers: prefetch side records, then try to format according to country
all_partner_ids = set(values['partner_id'] for values in vals_list if values.get('partner_id'))
all_event_ids = set(values['event_id'] for values in vals_list if values.get('event_id'))
for values in vals_list:
if not values.get('phone') and not values.get('mobile'):
continue
related_country = self.env['res.country']
if values.get('partner_id'):
related_country = self.env['res.partner'].with_prefetch(all_partner_ids).browse(values['partner_id']).country_id
if not related_country and values.get('event_id'):
related_country = self.env['event.event'].with_prefetch(all_event_ids).browse(values['event_id']).country_id
if not related_country:
related_country = self.env.company.country_id
for fname in {'mobile', 'phone'}:
if values.get(fname):
values[fname] = self._phone_format(values[fname], related_country)
registrations = super(EventRegistration, self).create(vals_list)
# auto_confirm if possible; if not automatically confirmed, call mail schedulers in case
@@ -181,6 +221,20 @@ class EventRegistration(models.Model):
""" Checks that all registrations are for `auto-confirm` events. """
return all(event.auto_confirm for event in self.event_id)
def _phone_format(self, number, country):
""" Call phone_validation formatting tool function. Returns original
number in case formatting cannot be done (no country, wrong info, ...) """
if not number or not country:
return number
new_number = phone_format(
number,
country.code,
country.phone_code,
force_format='E164',
raise_exception=False,
)
return new_number if new_number else number
# ------------------------------------------------------------
# ACTIONS / BUSINESS
# ------------------------------------------------------------
+124 -3
View File
@@ -8,7 +8,7 @@ from odoo import Command
from odoo.addons.event.tests.common import EventCase
from odoo import exceptions
from odoo.fields import Datetime as FieldsDatetime
from odoo.tests.common import users, Form
from odoo.tests.common import users, Form, tagged
from odoo.tools import mute_logger
@@ -55,6 +55,7 @@ class TestEventInternalsCommon(EventCase):
})
@tagged('event_event')
class TestEventData(TestEventInternalsCommon):
@users('user_eventmanager')
@@ -573,6 +574,7 @@ class TestEventData(TestEventInternalsCommon):
self.env['event.registration'].create(new_draft_to_autoconfirm)
@tagged('event_registration')
class TestEventRegistrationData(TestEventInternalsCommon):
@users('user_eventmanager')
@@ -580,6 +582,7 @@ class TestEventRegistrationData(TestEventInternalsCommon):
""" Test registration computed fields about partner """
test_email = '"Nibbler In Space" <nibbler@futurama.example.com>'
test_phone = '0456001122'
test_phone_fmt = '+32456001122'
event = self.env['event.event'].browse(self.event_0.ids)
customer = self.env['res.partner'].browse(self.event_customer.id)
@@ -626,12 +629,12 @@ class TestEventRegistrationData(TestEventInternalsCommon):
new_reg = event.registration_ids.sorted()[0]
self.assertEqual(new_reg.name, 'Nibbler In Space')
self.assertEqual(new_reg.email, False)
self.assertEqual(new_reg.phone, test_phone)
self.assertEqual(new_reg.phone, test_phone_fmt)
new_reg.write({'partner_id': customer.id})
self.assertEqual(new_reg.partner_id, customer)
self.assertEqual(new_reg.name, 'Nibbler In Space')
self.assertEqual(new_reg.email, customer.email)
self.assertEqual(new_reg.phone, test_phone)
self.assertEqual(new_reg.phone, test_phone_fmt)
@users('user_eventmanager')
def test_registration_partner_sync_company(self):
@@ -667,6 +670,123 @@ class TestEventRegistrationData(TestEventInternalsCommon):
self.assertEqual(new_reg.phone, contact.phone)
@tagged('event_registration', 'phone_number')
class TestEventRegistrationPhone(EventCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_event_address = cls.env['res.partner'].create({
'city': 'Gandhinagar',
'country_id': cls.env.ref("base.in").id,
'name': 'Odoo In',
'zip': '382007',
})
cls.test_event = cls.env['event.event'].create({
'address_id': cls.test_event_address.id,
'company_id': cls.company_admin.id,
'date_begin': datetime(2023, 6, 5, 8, 0, 0),
'date_end': datetime(2023, 6, 8, 18, 0, 0),
'name': 'Test Phone Format',
})
@users('user_eventregistrationdesk')
def test_assert_initial_values(self):
customer = self.event_customer.with_env(self.env)
customer2 = self.event_customer2.with_env(self.env)
event = self.test_event.with_env(self.env)
self.assertFalse(customer.mobile)
self.assertEqual(customer.phone, '0485112233')
self.assertEqual(customer2.mobile, '0456654321')
self.assertEqual(customer2.phone, '0456987654')
self.assertEqual(event.company_id.country_id, self.env.ref("base.be"))
self.assertEqual(event.country_id, self.env.ref("base.in"))
@users('user_eventregistrationdesk')
def test_registration_form_phone(self):
""" Test onchange on phone / mobile, should try to format number """
event = self.test_event.with_user(self.env.user)
lead_form = Form(self.env['event.registration'])
lead_form.event_id = event
lead_form.mobile = '7200000011'
lead_form.phone = '7200000000'
self.assertEqual(lead_form.mobile, '+917200000011')
self.assertEqual(lead_form.phone, '+917200000000')
@users('user_eventregistrationdesk')
def test_registration_phone_format(self):
""" Test phone formatting: based on partner (BE numbers) or event
(IN numbers) or company (BE numbers). """
event = self.test_event.with_user(self.env.user)
# customer_id, mobile, phone -> based on partner or event country
sources = [
(self.event_customer.id, None, None), # BE local on partner
(self.event_customer2.id, None, None), # BE local on partner
(self.event_customer2.id, '0456001122', None), # BE local + on partner
(False, '0456778899', '+32456778899'), # BE local + BE global
(False, '7200000000', False), # IN local
(False, False, '7200000011'), # IN local
(False, '7200000000', '7200000011'), # IN local
(False, '+917200000088', '+917200000099'), # IN global
]
# mobile, phone
expected = [
(False, '0485112233'), # partner values, no format
('0456654321', '0456987654'), # partner values, no format
('+32456001122', '0456987654'), # BE on partner / partner value, no format
('0456778899', '+32456778899'), # IN on event -> cannot format BE
('+917200000000', False), # IN on event
(False, '+917200000011'), # IN on event
('+917200000000', '+917200000011'), # IN on event
('+917200000088', '+917200000099'), # already formatted
]
for (partner_id, mobile, phone), (exp_mobile, exp_phone) in zip(sources, expected):
with self.subTest(partner_id=partner_id, mobile=mobile, phone=phone):
create_vals = {
'event_id': event.id,
'partner_id': partner_id,
}
if mobile is not None:
create_vals['mobile'] = mobile
if phone is not None:
create_vals['phone'] = phone
reg = self.env['event.registration'].create(create_vals)
self.assertEqual(reg.mobile, exp_mobile)
self.assertEqual(reg.phone, exp_phone)
# no country on event -> based on partner or event company country
self.test_event.write({'address_id': False})
expected = [
(False, '0485112233'), # partner values, no format
('0456654321', '0456987654'), # partner values, no format
('+32456001122', '0456987654'), # BE on partner / partner value, no format
('+32456778899', '+32456778899'), # BE on company
('7200000000', False), # BE on company -> cannot format IN
(False, '7200000011'), # BE on company -> cannot format IN
('7200000000', '7200000011'), # BE on company -> cannot format IN
('+917200000088', '+917200000099'), # already formatted
]
for (partner_id, mobile, phone), (exp_mobile, exp_phone) in zip(sources, expected):
with self.subTest(partner_id=partner_id, mobile=mobile, phone=phone):
create_vals = {
'event_id': event.id,
'partner_id': partner_id,
}
if mobile is not None:
create_vals['mobile'] = mobile
if phone is not None:
create_vals['phone'] = phone
reg = self.env['event.registration'].create(create_vals)
self.assertEqual(reg.mobile, exp_mobile)
self.assertEqual(reg.phone, exp_phone)
@tagged('event_ticket')
class TestEventTicketData(TestEventInternalsCommon):
@freeze_time('2020-1-31 10:00:00')
@@ -829,6 +949,7 @@ class TestEventTicketData(TestEventInternalsCommon):
reg_draft.write({'state': 'open'})
@tagged('event_event')
class TestEventTypeData(TestEventInternalsCommon):
@users('user_eventmanager')
+20 -5
View File
@@ -4,6 +4,7 @@
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.addons.phone_validation.tools import phone_validation
class EventRegistration(models.Model):
@@ -188,16 +189,30 @@ class EventRegistration(models.Model):
:return dict: values used for create / write on a lead
"""
valid_partner = related_partner = next(
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
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):
# mono registration mode: keep partner only if email and phone matches;
# otherwise registration > partner. Note that _phone_format has to be
# taken into account as it may have side effects on phone formatting
if len(self) == 1 and valid_partner:
if self.email and valid_partner.email and valid_partner.email != self.email:
valid_partner = self.env['res.partner']
if valid_partner and self.phone and valid_partner.phone:
phone_formatted = phone_validation.phone_format(
self.phone,
valid_partner.country_id.code or None,
valid_partner.country_id.phone_code or None,
force_format='E164',
raise_exception=False
)
partner_phone_formatted = valid_partner._phone_format(valid_partner.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']
if valid_partner:
contact_vals = self.env['crm.lead']._prepare_values_from_partner(valid_partner)
+5 -3
View File
@@ -168,14 +168,16 @@ class TestEventSale(TestEventSaleCommon):
)
self.assertEqual(
set(ticket1_new_reg.mapped('phone')),
set(['+32456111111', self.event_customer.phone])
set(['+32456111111', self.event_customer._phone_format(self.event_customer.phone)])
)
self.assertEqual(
set(ticket1_new_reg.mapped('mobile')),
set(['+32456222222', self.event_customer.mobile])
set(['+32456222222', self.event_customer._phone_format(self.event_customer.mobile)])
)
for field in ['name', 'email', 'phone', 'mobile']:
for field in ['name', 'email']:
self.assertEqual(ticket2_new_reg[field], self.event_customer[field])
for field in ['phone', 'mobile']:
self.assertEqual(ticket2_new_reg[field], self.event_customer._phone_format(self.event_customer[field]))
# ADDING MANUAL LINES ON SO
# ------------------------------------------------------------