diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index cc3cac72253..3d8dbb4bb7e 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -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 # ------------------------------------------------------------ diff --git a/addons/event/tests/test_event_internals.py b/addons/event/tests/test_event_internals.py index 9d1986738c7..db4f1ed3a16 100644 --- a/addons/event/tests/test_event_internals.py +++ b/addons/event/tests/test_event_internals.py @@ -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" ' 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') diff --git a/addons/event_crm/models/event_registration.py b/addons/event_crm/models/event_registration.py index e370a99f598..2fd8e9e4ac1 100644 --- a/addons/event_crm/models/event_registration.py +++ b/addons/event_crm/models/event_registration.py @@ -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) diff --git a/addons/event_sale/tests/test_event_sale.py b/addons/event_sale/tests/test_event_sale.py index d2662d4bfab..e1a9fb40bcf 100644 --- a/addons/event_sale/tests/test_event_sale.py +++ b/addons/event_sale/tests/test_event_sale.py @@ -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 # ------------------------------------------------------------