Previously a blacklisted phone number or email address could only be
removed from their respective blacklist by going to the corresponding
blacklist view [in mass_mailing_(sms)] and manually
archiving/deactiviting the entry. All users could see that an email was
blacklisted via an fa-ban icon added next to their corresponding field
in the modules: crm, mass_mailing, mass_mailing_sms, and contacts (via
extension).
This commit adds additional fa-ban icons next to blacklisted phone
numbers and makes all instances of these icons clickable to remove them
from their corresponding blacklist using a wizard. There are several
limitations to this implementation including:
1. If both a mobile and phone number field appear within a crm lead
instance, then only the mobile field will indicate if it is blacklisted
(due to current implementation of PhoneMixin which only checks 1 phone
value against the blacklist and "sms" which returns mobile numbers first).
I.e. if a phone field value is blacklisted, but a value is typed into the
mobile field, then the phone field will never indicate that it is blacklisted.
2. If someone clicks on a unblacklisting icon after changing the
corresponding field value without clicking off the field input, then the
latest typed in value will be the one submitted for unblacklisting,
which may not actually be blacklisted (due to "is_blacklisted" flag
being a computed field and it not having a chance to re-compute to hide
the button).
3. If someone changes values in the blacklist, then someone who already
has a form open will not see icon disappear until something triggers a
re-compute of the "phone_sanitized_blacklisted" flag (this was already the
case with the icon, but now a user may try to unblacklist a value that is
already unblacklisted.)
4. Since the icon needs to be visible by everyone to indicate whether or
not a phone/email is blacklisted, users without corresponding unblacklist
permissions will be able to click on the icon. When they click on it, they
will be informed they do not have permission to unblacklist. A wizard was
determined to be the best way to implement this unblacklisting for the
following reasons:
- A "Unblacklisting Reason" is needed and will not be stored as a field.
- A field widget is unable to do this due to security settings +
inability to refresh view after unblacklisting with a
"Unblacklisting Reason" without wiping unsaved changes.
Additionally, to better align with GDPR, the corresponding form view for
the phone/email blacklist has been updated to use the same wizard to
track "Reason for unblacklisting". Unfortunately there is no
straightforward way to prevent direct "Archive" action, so users are
still able to bypass unblacklisting without being asked for a "reason
for unblacklisting".
Supports task: 2117635
Upgrade PR: odoo/upgrade#939
COM PR: odoo/odoo#45315
118 lines
4.9 KiB
Python
118 lines
4.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import _
|
|
from odoo.exceptions import UserError
|
|
|
|
import logging
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
_phonenumbers_lib_warning = False
|
|
|
|
|
|
try:
|
|
import phonenumbers
|
|
|
|
def phone_parse(number, country_code):
|
|
try:
|
|
phone_nbr = phonenumbers.parse(number, region=country_code, keep_raw_input=True)
|
|
except phonenumbers.phonenumberutil.NumberParseException as e:
|
|
raise UserError(_('Unable to parse %s.: %s') % (number, str(e)))
|
|
|
|
if not phonenumbers.is_possible_number(phone_nbr):
|
|
raise UserError(_('Impossible number %s: probably invalid number of digits.') % number)
|
|
if not phonenumbers.is_valid_number(phone_nbr):
|
|
raise UserError(_('Invalid number %s: probably incorrect prefix.') % number)
|
|
|
|
return phone_nbr
|
|
|
|
def phone_format(number, country_code, country_phone_code, force_format='INTERNATIONAL', raise_exception=True):
|
|
""" Format the given phone number according to the localisation and international options.
|
|
:param number: number to convert
|
|
:param country_code: the ISO country code in two chars
|
|
:type country_code: str
|
|
:param country_phone_code: country dial in codes, defined by the ITU-T (Ex: 32 for Belgium)
|
|
:type country_phone_code: int
|
|
:param force_format: stringified version of format globals (see
|
|
https://github.com/daviddrysdale/python-phonenumbers/blob/dev/python/phonenumbers/phonenumberutil.py)
|
|
'E164' = 0
|
|
'INTERNATIONAL' = 1
|
|
'NATIONAL' = 2
|
|
'RFC3966' = 3
|
|
:type force_format: str
|
|
:rtype: str
|
|
"""
|
|
try:
|
|
phone_nbr = phone_parse(number, country_code)
|
|
except (phonenumbers.phonenumberutil.NumberParseException, UserError) as e:
|
|
if raise_exception:
|
|
raise
|
|
else:
|
|
_logger.warning(_('Unable to format %s:\n%s'), number, e)
|
|
return number
|
|
if force_format == 'E164':
|
|
phone_fmt = phonenumbers.PhoneNumberFormat.E164
|
|
elif force_format == 'RFC3966':
|
|
phone_fmt = phonenumbers.PhoneNumberFormat.RFC3966
|
|
elif force_format == 'INTERNATIONAL' or phone_nbr.country_code != country_phone_code:
|
|
phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
|
|
else:
|
|
phone_fmt = phonenumbers.PhoneNumberFormat.NATIONAL
|
|
return phonenumbers.format_number(phone_nbr, phone_fmt)
|
|
|
|
except ImportError:
|
|
|
|
def phone_parse(number, country_code):
|
|
return False
|
|
|
|
def phone_format(number, country_code, country_phone_code, force_format='INTERNATIONAL', raise_exception=True):
|
|
global _phonenumbers_lib_warning
|
|
if not _phonenumbers_lib_warning:
|
|
_logger.info(
|
|
"The `phonenumbers` Python module is not installed, contact numbers will not be "
|
|
"verified. Please install the `phonenumbers` Python module."
|
|
)
|
|
_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)
|