From adfdf5ab33e719a55f65e2d9ff34ff265fe1b579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 12 Jul 2023 10:37:07 +0000 Subject: [PATCH] [IMP] phone_validation: add a phone formatting tool on model RATIONALE Simplify field management for mail / phone / sms flows. Make it working out of the box, easier to use and tweak. SPECIFICATIONS Add a '_phone_format' tool method on BaseModel. It allows to format a number either directly, either from a field available on the model. It allows to ease number formatting. It uses available helpers to find numbers using '_phone_get_number_fields' and '_phone_get_country_field'. With default generic behavior this allows to simplify most calls to phone number formatting. Having it available at BaseModel level allows to remove some custom code, calls to phone_validation API, ... Task-3422449 (Mail, Phone: Move and improve field helpers) Part-of: odoo/odoo#130468 --- addons/crm/models/crm_lead.py | 8 +-- addons/event/__manifest__.py | 2 +- addons/event/models/event_registration.py | 29 +------- addons/event_crm/models/event_registration.py | 10 +-- addons/event_sale/tests/test_event_sale.py | 6 +- addons/mass_mailing_sms/controllers/main.py | 16 +++-- .../wizard/mailing_sms_test.py | 5 +- .../models/payment_transaction.py | 15 ++--- .../models/mail_thread_phone.py | 21 +----- addons/phone_validation/models/models.py | 66 ++++++++++++++++++- .../models/phone_blacklist.py | 35 +++++----- addons/phone_validation/models/res_partner.py | 24 +------ addons/phone_validation/models/res_users.py | 12 ++-- .../tools/phone_validation.py | 50 ++------------ addons/sms/models/mail_thread.py | 13 ++-- addons/sms/tests/common.py | 6 +- addons/sms/wizard/sms_composer.py | 8 +-- .../test_mail_sms/tests/test_sms_composer.py | 2 +- addons/test_mail_sms/tests/test_sms_post.py | 6 +- .../website_crm/controllers/website_form.py | 4 +- 20 files changed, 144 insertions(+), 194 deletions(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 54848433f96..1bcea9ed084 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -633,12 +633,12 @@ class Lead(models.Model): @api.onchange('phone', 'country_id', 'company_id') def _onchange_phone_validation(self): if self.phone: - self.phone = self.phone_get_sanitized_number(number_fname='phone', force_format='INTERNATIONAL') or self.phone + self.phone = self._phone_format(fname='phone', force_format='INTERNATIONAL') or self.phone @api.onchange('mobile', 'country_id', 'company_id') def _onchange_mobile_validation(self): if self.mobile: - self.mobile = self.phone_get_sanitized_number(number_fname='mobile', force_format='INTERNATIONAL') or self.mobile + self.mobile = self._phone_format(fname='mobile', force_format='INTERNATIONAL') or self.mobile def _prepare_values_from_partner(self, partner): """ Get a dictionary with values coming from partner information to @@ -706,8 +706,8 @@ class Lead(models.Model): """ self.ensure_one() if self.partner_id and self.phone != self.partner_id.phone: - lead_phone_formatted = self.phone_get_sanitized_number(number_fname='phone') or self.phone or False - partner_phone_formatted = self.partner_id.phone_get_sanitized_number(number_fname='phone') or self.partner_id.phone or False + lead_phone_formatted = self._phone_format(fname='phone') or self.phone or False + partner_phone_formatted = self.partner_id._phone_format(fname='phone') or self.partner_id.phone or False return lead_phone_formatted != partner_phone_formatted return False diff --git a/addons/event/__manifest__.py b/addons/event/__manifest__.py index 93909169673..7f0863d3cde 100644 --- a/addons/event/__manifest__.py +++ b/addons/event/__manifest__.py @@ -17,7 +17,7 @@ Key Features * Manage your Events and Registrations * Use emails to automatically confirm and send acknowledgments for any event registration """, - 'depends': ['base_setup', 'mail', 'portal', 'utm'], + 'depends': ['base_setup', 'mail', 'phone_validation', 'portal', 'utm'], 'data': [ 'security/event_security.xml', 'security/ir.model.access.csv', diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index 3d8dbb4bb7e..44261ae5b86 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -7,15 +7,6 @@ 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' @@ -134,13 +125,13 @@ class EventRegistration(models.Model): 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) + self.phone = self._phone_format(fname='phone', country=country) or self.phone @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) + self.mobile = self._phone_format(fname='mobile', country=country) # ------------------------------------------------------------ # CRUD @@ -165,7 +156,7 @@ class EventRegistration(models.Model): for fname in {'mobile', 'phone'}: if values.get(fname): - values[fname] = self._phone_format(values[fname], related_country) + values[fname] = self._phone_format(number=values[fname], country=related_country) or values[fname] registrations = super(EventRegistration, self).create(vals_list) @@ -221,20 +212,6 @@ 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_crm/models/event_registration.py b/addons/event_crm/models/event_registration.py index 1abca8f3602..d84556811c7 100644 --- a/addons/event_crm/models/event_registration.py +++ b/addons/event_crm/models/event_registration.py @@ -208,14 +208,8 @@ class EventRegistration(models.Model): # compare phone, taking into account formatting 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) + 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: diff --git a/addons/event_sale/tests/test_event_sale.py b/addons/event_sale/tests/test_event_sale.py index e1a9fb40bcf..cf1ea2756d8 100644 --- a/addons/event_sale/tests/test_event_sale.py +++ b/addons/event_sale/tests/test_event_sale.py @@ -168,16 +168,16 @@ class TestEventSale(TestEventSaleCommon): ) self.assertEqual( set(ticket1_new_reg.mapped('phone')), - set(['+32456111111', self.event_customer._phone_format(self.event_customer.phone)]) + set(['+32456111111', self.event_customer._phone_format(fname='phone')]) ) self.assertEqual( set(ticket1_new_reg.mapped('mobile')), - set(['+32456222222', self.event_customer._phone_format(self.event_customer.mobile)]) + set(['+32456222222', self.event_customer._phone_format(fname='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])) + self.assertEqual(ticket2_new_reg[field], self.event_customer._phone_format(fname=field)) # ADDING MANUAL LINES ON SO # ------------------------------------------------------------ diff --git a/addons/mass_mailing_sms/controllers/main.py b/addons/mass_mailing_sms/controllers/main.py index 4ac1d7a0ea5..858a2f0494a 100644 --- a/addons/mass_mailing_sms/controllers/main.py +++ b/addons/mass_mailing_sms/controllers/main.py @@ -43,17 +43,21 @@ class MailingSMSController(http.Controller): return request.redirect('/web') # parse and validate number sms_number = post.get('sms_number', '').strip(' ') - sanitize_res = phone_validation.phone_sanitize_numbers([sms_number], request.geoip.country_code, None)[sms_number] - tocheck_number = sanitize_res['sanitized'] or sms_number - - trace = check_res['trace'].filtered(lambda r: r.sms_number == tocheck_number)[:1] - mailing_list_ids = trace.mass_mailing_id.contact_list_ids + sanitized = phone_validation.phone_format( + sms_number, + request.geoip.country_code, + force_format='E164', + raise_exception=False, + ) + tocheck_number = sanitized or sms_number + trace = check_res['trace'].filtered(lambda r: r.sms_number == tocheck_number)[:1] if tocheck_number else False # compute opt-out / blacklist information lists_optout = request.env['mailing.list'].sudo() lists_optin = request.env['mailing.list'].sudo() unsubscribe_error = False if tocheck_number and trace: + mailing_list_ids = trace.mass_mailing_id.contact_list_ids if mailing_list_ids: subscriptions = request.env['mailing.contact.subscription'].sudo().search([ ('list_id', 'in', mailing_list_ids.ids), @@ -74,7 +78,7 @@ class MailingSMSController(http.Controller): elif tocheck_number: unsubscribe_error = _('Number %s not found', tocheck_number) else: - unsubscribe_error = sanitize_res['msg'] + unsubscribe_error = _('Invalid number %s', post.get('sms_number', '')) return request.render('mass_mailing_sms.blacklist_number', { 'mailing_id': mailing_id, diff --git a/addons/mass_mailing_sms/wizard/mailing_sms_test.py b/addons/mass_mailing_sms/wizard/mailing_sms_test.py index 7ba96ffbf5b..be10bd8d7a6 100644 --- a/addons/mass_mailing_sms/wizard/mailing_sms_test.py +++ b/addons/mass_mailing_sms/wizard/mailing_sms_test.py @@ -21,9 +21,8 @@ class MassSMSTest(models.TransientModel): self.ensure_one() numbers = [number.strip() for number in self.numbers.splitlines()] - sanitize_res = phone_validation.phone_sanitize_numbers_w_record(numbers, self.env.user) - sanitized_numbers = [info['sanitized'] for info in sanitize_res.values() if info['sanitized']] - invalid_numbers = [number for number, info in sanitize_res.items() if info['code']] + sanitized_numbers = [self.env.user._phone_format(number=number) for number in numbers] + invalid_numbers = [number for sanitized, number in zip(sanitized_numbers, numbers) if not sanitized] record = self.env[self.mailing_id.mailing_model_real].search([], limit=1) body = self.mailing_id.body_plaintext diff --git a/addons/payment_razorpay/models/payment_transaction.py b/addons/payment_razorpay/models/payment_transaction.py index 802f10ea2a4..46bb125238e 100644 --- a/addons/payment_razorpay/models/payment_transaction.py +++ b/addons/payment_razorpay/models/payment_transaction.py @@ -11,7 +11,6 @@ from odoo.exceptions import UserError, ValidationError from odoo.addons.payment import utils as payment_utils from odoo.addons.payment_razorpay.const import PAYMENT_STATUS_MAPPING from odoo.addons.payment_razorpay.controllers.main import RazorpayController -from odoo.addons.phone_validation.tools.phone_validation import phone_sanitize_numbers _logger = logging.getLogger(__name__) @@ -52,16 +51,14 @@ class PaymentTransaction(models.Model): return_url_params = {'reference': self.reference} phone = self.partner_phone - error_message = _("The phone number is missing.") if phone: # sanitize partner phone - country_code = self.partner_country_id.code - country_phone_code = self.partner_country_id.phone_code - phone_info = phone_sanitize_numbers([phone], country_code, country_phone_code) - phone = phone_info[self.partner_phone]['sanitized'] - error_message = phone_info[self.partner_phone]['msg'] - if not phone: - raise ValidationError("Razorpay: " + error_message) + try: + phone = self._phone_format(number=phone, country=self.partner_country_id, raise_exception=True) + except Exception as err: + raise ValidationError("Razorpay: " + str(err)) from err + else: + raise ValidationError("Razorpay: " + _("The phone number is missing.")) rendering_values = { 'key_id': self.provider_id.razorpay_key_id, diff --git a/addons/phone_validation/models/mail_thread_phone.py b/addons/phone_validation/models/mail_thread_phone.py index 184e79e1b90..e369f376f22 100644 --- a/addons/phone_validation/models/mail_thread_phone.py +++ b/addons/phone_validation/models/mail_thread_phone.py @@ -4,7 +4,6 @@ import re from odoo import api, fields, models, _ -from odoo.addons.phone_validation.tools import phone_validation from odoo.exceptions import AccessError, UserError from odoo.osv import expression @@ -127,7 +126,7 @@ class PhoneMixin(models.AbstractModel): number_fields = self._phone_get_number_fields() for record in self: for fname in number_fields: - sanitized = record.phone_get_sanitized_number(number_fname=fname) + sanitized = record._phone_format(fname=fname) if sanitized: break record.phone_sanitized = sanitized @@ -148,9 +147,9 @@ class PhoneMixin(models.AbstractModel): # may not be calculated as blacklisted even though it is if both field values exist in a model. for number_field in number_fields: if 'mobile' in number_field: - mobile_blacklisted = record.phone_sanitized_blacklisted and record.phone_get_sanitized_number(number_fname=number_field) == record.phone_sanitized + mobile_blacklisted = record.phone_sanitized_blacklisted and record._phone_format(fname=number_field) == record.phone_sanitized else: - phone_blacklisted = record.phone_sanitized_blacklisted and record.phone_get_sanitized_number(number_fname=number_field) == record.phone_sanitized + phone_blacklisted = record.phone_sanitized_blacklisted and record._phone_format(fname=number_field) == record.phone_sanitized record.mobile_blacklisted = mobile_blacklisted record.phone_blacklisted = phone_blacklisted @@ -196,20 +195,6 @@ class PhoneMixin(models.AbstractModel): res = [self._phone_get_country_field()] if self._phone_get_country_field() else [] return res + self._phone_get_number_fields() - def phone_get_sanitized_numbers(self, number_fname='mobile', force_format='E164'): - res = dict.fromkeys(self.ids, False) - country_fname = self._phone_get_country_field() - for record in self: - number = record[number_fname] - res[record.id] = phone_validation.phone_sanitize_numbers_w_record([number], record, record_country_fname=country_fname, force_format=force_format)[number]['sanitized'] - return res - - def phone_get_sanitized_number(self, number_fname='mobile', force_format='E164'): - self.ensure_one() - country_fname = self._phone_get_country_field() - number = self[number_fname] - return phone_validation.phone_sanitize_numbers_w_record([number], self, record_country_fname=country_fname, force_format=force_format)[number]['sanitized'] - def _phone_set_blacklisted(self): return self.env['phone.blacklist'].sudo()._add([r.phone_sanitized for r in self]) diff --git a/addons/phone_validation/models/models.py b/addons/phone_validation/models/models.py index 68422b96496..a66d5cff5e5 100644 --- a/addons/phone_validation/models/models.py +++ b/addons/phone_validation/models/models.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, models +from odoo import api, exceptions, models +from odoo.addons.phone_validation.tools import phone_validation class BaseModel(models.AbstractModel): @@ -24,3 +25,66 @@ class BaseModel(models.AbstractModel): if 'country_id' in self: return 'country_id' return False + + def _phone_format(self, fname=False, number=False, country=False, force_format='E164', raise_exception=False): + """ Format and return number. This number can be found using a field + (in which case self should be a singleton recordet), or directly given + if the formatting itself is what matter. Field name can be found + automatically using '_phone_get_number_fields' + + :param str fname: if number is not given, fname indicates the field to + use to find the number; otherwise use '_phone_get_number_fields'; + :param str number: number to format (in which case fields-based computation + is skipped); + :param country: country used for formatting number; otherwise + it is fetched based on record, using '_phone_get_country_field'; + :param str force_format: stringified version of format globals; should be + one of 'E164', 'INTERNATIONAL', 'NATIONAL' or 'RFC3966'; + :param bool raise_exception: raise if formatting is not possible (notably + wrong formatting, invalid country information, ...). Otherwise False + is returned; + + :return str: formatted number. If formatting is not possible False is + returned. + """ + if not number: + # if no number is given, having a singletong recordset is mandatory to + # always have a number as input + self.ensure_one() + fnames = self._phone_get_number_fields() if not fname else [fname] + number = next((self[fname] for fname in fnames if fname in self and self[fname]), False) + if not number: + return False + + # fetch country info only if self is a singleton recordset allowing to + # effectively try to find a country + if not country and self: + self.ensure_one() + country_fname = self._phone_get_country_field() + country = self[country_fname] if country_fname and country_fname in self else self.env['res.country'] + if not country: + country = self.env.company.country_id + if not country: + return number + + return self._phone_format_number(number, country=country, force_format=force_format, raise_exception=raise_exception) + + def _phone_format_number(self, number, country, force_format='E164', raise_exception=False): + """ Format and return number according to the asked format. This is + mainly a small helper around 'phone_validation.phone_format'.""" + if not number or not country: + return False + + try: + number = phone_validation.phone_format( + number, + country.code, + country.phone_code, + force_format=force_format, + raise_exception=True, # do not get original number returned + ) + except exceptions.UserError: + if raise_exception: + raise + number = False + return number diff --git a/addons/phone_validation/models/phone_blacklist.py b/addons/phone_validation/models/phone_blacklist.py index 7ec2dbbb836..ebcb8eeccb2 100644 --- a/addons/phone_validation/models/phone_blacklist.py +++ b/addons/phone_validation/models/phone_blacklist.py @@ -4,7 +4,6 @@ import logging from odoo import api, fields, models, _ -from odoo.addons.phone_validation.tools import phone_validation from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @@ -26,21 +25,20 @@ class PhoneBlackList(models.Model): @api.model_create_multi def create(self, values): - # First of all, extract values to ensure emails are really unique (and don't modify values in place) + # First of all, extract values to ensure numbers are really unique (and don't modify values in place) to_create = [] done = set() for value in values: - number = value['number'] - sanitized_values = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number] - sanitized = sanitized_values['sanitized'] - if not sanitized: - raise UserError(sanitized_values['msg'] + _(" Please correct the number and try again.")) - if sanitized in done: + try: + sanitized_value = self.env.user._phone_format(number=value['number'], raise_exception=True) + except UserError as err: + raise UserError(str(err) + _(" Please correct the number and try again.")) from err + if sanitized_value in done: continue - done.add(sanitized) - to_create.append(dict(value, number=sanitized)) + done.add(sanitized_value) + to_create.append(dict(value, number=sanitized_value)) - """ To avoid crash during import due to unique email, return the existing records if any """ + # To avoid crash during import due to unique number, return the existing records if any bl_entries = {} if to_create: sql = '''SELECT number, id FROM phone_blacklist WHERE number = ANY(%s)''' @@ -54,11 +52,10 @@ class PhoneBlackList(models.Model): def write(self, values): if 'number' in values: - number = values['number'] - sanitized_values = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number] - sanitized = sanitized_values['sanitized'] - if not sanitized: - raise UserError(sanitized_values['msg'] + _(" Please correct the number and try again.")) + try: + sanitized = self.env.user._phone_format(number=values['number'], raise_exception=True) + except UserError as err: + raise UserError(str(err) + _(" Please correct the number and try again.")) from err values['number'] = sanitized return super(PhoneBlackList, self).write(values) @@ -67,7 +64,7 @@ class PhoneBlackList(models.Model): def sanitize_number(arg): if isinstance(arg, (list, tuple)) and arg[0] == 'number' and isinstance(arg[2], str): number = arg[2] - sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized'] + sanitized = self.env.user._phone_format(number=number) if sanitized: return (arg[0], arg[1], sanitized) return arg @@ -76,7 +73,7 @@ class PhoneBlackList(models.Model): return super()._search(domain, offset, limit, order, access_rights_uid) def add(self, number, message=None): - sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized'] + sanitized = self.env.user._phone_format(number=number) return self._add([sanitized], message=message) def _add(self, numbers, message=None): @@ -101,7 +98,7 @@ class PhoneBlackList(models.Model): return records def remove(self, number, message=None): - sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized'] + sanitized = self.env.user._phone_format(number=number) return self._remove([sanitized], message=message) def _remove(self, numbers, message=None): diff --git a/addons/phone_validation/models/res_partner.py b/addons/phone_validation/models/res_partner.py index c93b466da61..292d0c131bc 100644 --- a/addons/phone_validation/models/res_partner.py +++ b/addons/phone_validation/models/res_partner.py @@ -11,29 +11,9 @@ class Partner(models.Model): @api.onchange('phone', 'country_id', 'company_id') def _onchange_phone_validation(self): if self.phone: - self.phone = self._phone_format(self.phone, force_format='INTERNATIONAL') + self.phone = self._phone_format(fname='phone', force_format='INTERNATIONAL') or self.phone @api.onchange('mobile', 'country_id', 'company_id') def _onchange_mobile_validation(self): if self.mobile: - self.mobile = self._phone_format(self.mobile, force_format='INTERNATIONAL') - - def _phone_format(self, number, country=None, company=None, force_format='E164'): - country = country or self.country_id or self.env.company.country_id - if not country or not number: - return number - return phone_validation.phone_format( - number, - country.code if country else None, - country.phone_code if country else None, - force_format=force_format, - raise_exception=False - ) - - def phone_get_sanitized_number(self, number_fname='mobile', force_format='E164'): - """ Stand alone version, allowing to use it on partner model without - having any dependency on sms module. To cleanup in master (15.3 +).""" - self.ensure_one() - country_fname = 'country_id' - number = self[number_fname] - return phone_validation.phone_sanitize_numbers_w_record([number], self, record_country_fname=country_fname, force_format=force_format)[number]['sanitized'] + self.mobile = self._phone_format(fname='mobile', force_format='INTERNATIONAL') or self.mobile diff --git a/addons/phone_validation/models/res_users.py b/addons/phone_validation/models/res_users.py index 467a2e04366..9a918ad283f 100644 --- a/addons/phone_validation/models/res_users.py +++ b/addons/phone_validation/models/res_users.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import _, models -from odoo.addons.phone_validation.tools import phone_validation class Users(models.Model): @@ -13,13 +12,10 @@ class Users(models.Model): numbers_to_blacklist = {} # numbers to blacklist and the related user if post.get('request_blacklist'): for user in self: - sanitized = phone_validation.phone_sanitize_numbers_w_record([user.phone, user.mobile], user) - user_phone = sanitized[user.phone]['sanitized'] - user_mobile = sanitized[user.mobile]['sanitized'] - if user_phone: - numbers_to_blacklist[user_phone] = user - if user_mobile: - numbers_to_blacklist[user_mobile] = user + for fname in self._phone_get_number_fields(): + number = user._phone_format(fname=fname) + if number: + numbers_to_blacklist[number] = user super(Users, self)._deactivate_portal_user(**post) diff --git a/addons/phone_validation/tools/phone_validation.py b/addons/phone_validation/tools/phone_validation.py index e6744669557..75f3ac5dbbf 100644 --- a/addons/phone_validation/tools/phone_validation.py +++ b/addons/phone_validation/tools/phone_validation.py @@ -17,7 +17,9 @@ try: try: phone_nbr = phonenumbers.parse(number, region=country_code or None, keep_raw_input=True) except phonenumbers.phonenumberutil.NumberParseException as e: - raise UserError(_('Unable to parse %(phone)s: %(error)s', phone=number, error=str(e))) + raise UserError( + _('Unable to parse %(phone)s: %(error)s', phone=number, error=str(e)) + ) from e if not phonenumbers.is_possible_number(phone_nbr): raise UserError(_('Impossible number %s: probably invalid number of digits.', number)) @@ -44,11 +46,10 @@ try: """ try: phone_nbr = phone_parse(number, country_code) - except (phonenumbers.phonenumberutil.NumberParseException, UserError) as e: + except UserError: if raise_exception: raise - else: - return number + return number if force_format == 'E164': phone_fmt = phonenumbers.PhoneNumberFormat.E164 elif force_format == 'RFC3966': @@ -73,44 +74,3 @@ except ImportError: ) _phonenumbers_lib_warning = True return number - - -def phone_sanitize_numbers(numbers, country_code, country_phone_code, force_format='E164'): - """ Given a list of numbers, return parsezd and sanitized information - - :return dict: {number: { - 'sanitized': sanitized and formated number or False (if cannot format) - 'code': 'empty' (number was a void string), 'invalid' (error) or False (sanitize ok) - 'msg': error message when 'invalid' - }} - """ - if not isinstance(numbers, (list)): - raise NotImplementedError() - result = dict.fromkeys(numbers, False) - for number in numbers: - if not number: - result[number] = {'sanitized': False, 'code': 'empty', 'msg': False} - continue - try: - stripped = number.strip() - sanitized = phone_format( - stripped, country_code, country_phone_code, - force_format=force_format, raise_exception=True) - except Exception as e: - result[number] = {'sanitized': False, 'code': 'invalid', 'msg': str(e)} - else: - result[number] = {'sanitized': sanitized, 'code': False, 'msg': False} - return result - - -def phone_sanitize_numbers_w_record(numbers, record, country=False, record_country_fname='country_id', force_format='E164'): - if not isinstance(numbers, (list)): - raise NotImplementedError() - if not country: - if record and record_country_fname and hasattr(record, record_country_fname) and record[record_country_fname]: - country = record[record_country_fname] - elif record: - country = record.env.company.country_id - country_code = country.code if country else None - country_phone_code = country.phone_code if country else None - return phone_sanitize_numbers(numbers, country_code, country_phone_code, force_format=force_format) diff --git a/addons/sms/models/mail_thread.py b/addons/sms/models/mail_thread.py index 7c6d6bc000a..971cbf64304 100644 --- a/addons/sms/models/mail_thread.py +++ b/addons/sms/models/mail_thread.py @@ -88,7 +88,7 @@ class MailThread(models.AbstractModel): valid_number = False for fname in [f for f in tocheck_fields if f in record]: - valid_number = phone_validation.phone_sanitize_numbers_w_record([record[fname]], record)[record[fname]]['sanitized'] + valid_number = record._phone_format(fname=fname) if valid_number: break @@ -104,7 +104,7 @@ class MailThread(models.AbstractModel): partner = self.env['res.partner'] for partner in all_partners: for fname in self.env['res.partner']._phone_get_number_fields(): - valid_number = phone_validation.phone_sanitize_numbers_w_record([partner[fname]], record)[partner[fname]]['sanitized'] + valid_number = partner._phone_format(fname=fname) if valid_number: break @@ -271,20 +271,17 @@ class MailThread(models.AbstractModel): if partner_ids: for partner in self.env['res.partner'].sudo().browse(partner_ids): number = sms_pid_to_number.get(partner.id) or partner.mobile or partner.phone - sanitize_res = phone_validation.phone_sanitize_numbers_w_record([number], partner)[number] - number = sanitize_res['sanitized'] or number sms_create_vals.append(dict( sms_base_vals, partner_id=partner.id, - number=number + number=partner._phone_format(number=number) or number, )) # notify from additional numbers if sms_numbers: - sanitized = phone_validation.phone_sanitize_numbers_w_record(sms_numbers, self) tocreate_numbers = [ - value['sanitized'] or original - for original, value in sanitized.items() + self._phone_format(number=sms_number) or sms_number + for sms_number in sms_numbers ] sms_create_vals += [dict( sms_base_vals, diff --git a/addons/sms/tests/common.py b/addons/sms/tests/common.py index b472384497e..ed4716629aa 100644 --- a/addons/sms/tests/common.py +++ b/addons/sms/tests/common.py @@ -93,7 +93,7 @@ class SMSCase(MockSMS): def _find_sms_sent(self, partner, number): if number is None and partner: - number = partner.phone_get_sanitized_number() + number = partner._phone_format() sent_sms = next((sms for sms in self._sms if sms['number'] == number), None) if not sent_sms: raise AssertionError('sent sms not found for %s (number: %s)' % (partner, number)) @@ -101,7 +101,7 @@ class SMSCase(MockSMS): def _find_sms_sms(self, partner, number, status): if number is None and partner: - number = partner.phone_get_sanitized_number() + number = partner._phone_format() domain = [('id', 'in', self._new_sms.ids), ('partner_id', '=', partner.id), ('number', '=', number)] @@ -214,7 +214,7 @@ class SMSCase(MockSMS): number = recipient_info.get('number') state = recipient_info.get('state', 'sent') if number is None and partner: - number = partner.phone_get_sanitized_number() + number = partner._phone_format() notif = notifications.filtered(lambda n: n.res_partner_id == partner and n.sms_number == number and n.notification_status == state) self.assertTrue(notif, 'SMS: not found notification for %s (number: %s, state: %s)' % (partner, number, state)) diff --git a/addons/sms/wizard/sms_composer.py b/addons/sms/wizard/sms_composer.py index 4dce9e3e945..8ace46e786f 100644 --- a/addons/sms/wizard/sms_composer.py +++ b/addons/sms/wizard/sms_composer.py @@ -136,8 +136,7 @@ class SendSMS(models.TransientModel): value = composer.recipient_single_number_itf or composer.recipient_single_number if value: records = composer._get_records() - sanitized = phone_validation.phone_sanitize_numbers_w_record([value], records)[value]['sanitized'] - composer.recipient_single_valid = bool(sanitized) + composer.recipient_single_valid = bool(records._phone_format(number=value)) if len(records) == 1 else False else: composer.recipient_single_valid = False @@ -147,9 +146,8 @@ class SendSMS(models.TransientModel): if composer.numbers: record = composer._get_records() if composer.res_model and composer.res_id else self.env.user numbers = [number.strip() for number in composer.numbers.split(',')] - sanitize_res = phone_validation.phone_sanitize_numbers_w_record(numbers, record) - sanitized_numbers = [info['sanitized'] for info in sanitize_res.values() if info['sanitized']] - invalid_numbers = [number for number, info in sanitize_res.items() if info['code']] + sanitized_numbers = [record._phone_format(number=number) for number in numbers] + invalid_numbers = [number for sanitized, number in zip(sanitized_numbers, numbers) if not sanitized] if invalid_numbers: raise UserError(_('Following numbers are not correctly encoded: %s', repr(invalid_numbers))) composer.sanitized_numbers = ','.join(sanitized_numbers) diff --git a/addons/test_mail_sms/tests/test_sms_composer.py b/addons/test_mail_sms/tests/test_sms_composer.py index 28fa8d2172c..25fb03a6f73 100644 --- a/addons/test_mail_sms/tests/test_sms_composer.py +++ b/addons/test_mail_sms/tests/test_sms_composer.py @@ -573,7 +573,7 @@ class TestSMSComposerMass(SMSCommon): with self.mockSMSGateway(): messages = composer._action_send_sms() - number = self.partners[2].phone_get_sanitized_number() + number = self.partners[2]._phone_format() self.assertSMSNotification( [{'partner': test_record_2.customer_id, 'number': number}], "Hello %s ceci est en français." % test_record_2.display_name, messages diff --git a/addons/test_mail_sms/tests/test_sms_post.py b/addons/test_mail_sms/tests/test_sms_post.py index f63258e703b..483294bb2f8 100644 --- a/addons/test_mail_sms/tests/test_sms_post.py +++ b/addons/test_mail_sms/tests/test_sms_post.py @@ -329,7 +329,7 @@ class TestSMSPostException(SMSCommon, TestSMSRecipients): ], self._test_body, messages) def test_message_sms_crash_credit_single(self): - with self.with_user('employee'), self.mockSMSGateway(nbr_t_error={self.partner_2.phone_get_sanitized_number(): 'credit'}): + with self.with_user('employee'), self.mockSMSGateway(nbr_t_error={self.partner_2._phone_format(): 'credit'}): test_record = self.env['mail.test.sms'].browse(self.test_record.id) messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids) @@ -361,7 +361,7 @@ class TestSMSPostException(SMSCommon, TestSMSRecipients): ], self._test_body, messages) def test_message_sms_crash_unregistered_single(self): - with self.with_user('employee'), self.mockSMSGateway(nbr_t_error={self.partner_2.phone_get_sanitized_number(): 'unregistered'}): + with self.with_user('employee'), self.mockSMSGateway(nbr_t_error={self.partner_2._phone_format(): 'unregistered'}): test_record = self.env['mail.test.sms'].browse(self.test_record.id) messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids) @@ -382,7 +382,7 @@ class TestSMSPostException(SMSCommon, TestSMSRecipients): ], self._test_body, messages) def test_message_sms_crash_wrong_number_single(self): - with self.with_user('employee'), self.mockSMSGateway(nbr_t_error={self.partner_2.phone_get_sanitized_number(): 'wrong_number_format'}): + with self.with_user('employee'), self.mockSMSGateway(nbr_t_error={self.partner_2._phone_format(): 'wrong_number_format'}): test_record = self.env['mail.test.sms'].browse(self.test_record.id) messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids) diff --git a/addons/website_crm/controllers/website_form.py b/addons/website_crm/controllers/website_form.py index 8071b663903..6b8fd7f1321 100644 --- a/addons/website_crm/controllers/website_form.py +++ b/addons/website_crm/controllers/website_form.py @@ -69,7 +69,9 @@ class WebsiteForm(form.WebsiteForm): # or if both numbers (after formating) are the same. This way we get additional phone # if possible, without modifying an existing one. (see inverse function on model crm.lead) if values_phone and visitor_partner.phone: - if visitor_partner._phone_format(visitor_partner.phone) == values_phone: + if values_phone == visitor_partner.phone: + values['partner_id'] = visitor_partner.id + elif (visitor_partner._phone_format('phone') or visitor_partner.phone) == values_phone: values['partner_id'] = visitor_partner.id else: values['partner_id'] = visitor_partner.id