218 lines
11 KiB
Python
218 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
||
import re
|
||
|
||
from odoo import api, fields, models, _
|
||
from odoo.exceptions import AccessError, UserError
|
||
from odoo.osv import expression
|
||
|
||
|
||
class PhoneMixin(models.AbstractModel):
|
||
""" Purpose of this mixin is to offer two services
|
||
|
||
* compute a sanitized phone number based on _phone_get_number_fields.
|
||
It takes first sanitized value, trying each field returned by the
|
||
method (see ``BaseModel._phone_get_number_fields()´´ for more details
|
||
about the usage of this method);
|
||
* compute blacklist state of records. It is based on phone.blacklist
|
||
model and give an easy-to-use field and API to manipulate blacklisted
|
||
records;
|
||
|
||
Main API methods
|
||
|
||
* ``_phone_set_blacklisted``: set recordset as blacklisted;
|
||
* ``_phone_reset_blacklisted``: reactivate recordset (even if not blacklisted
|
||
this method can be called safely);
|
||
"""
|
||
_name = 'mail.thread.phone'
|
||
_description = 'Phone Blacklist Mixin'
|
||
_inherit = ['mail.thread']
|
||
_phone_search_min_length = 3
|
||
|
||
phone_sanitized = fields.Char(
|
||
string='Sanitized Number', compute="_compute_phone_sanitized", compute_sudo=True, store=True,
|
||
help="Field used to store sanitized phone number. Helps speeding up searches and comparisons.")
|
||
phone_sanitized_blacklisted = fields.Boolean(
|
||
string='Phone Blacklisted', compute="_compute_blacklisted", compute_sudo=True, store=False,
|
||
search="_search_phone_sanitized_blacklisted", groups="base.group_user",
|
||
help="If the sanitized phone number is on the blacklist, the contact won't receive mass mailing sms anymore, from any list")
|
||
phone_blacklisted = fields.Boolean(
|
||
string='Blacklisted Phone is Phone', compute="_compute_blacklisted", compute_sudo=True, store=False, groups="base.group_user",
|
||
help="Indicates if a blacklisted sanitized phone number is a phone number. Helps distinguish which number is blacklisted \
|
||
when there is both a mobile and phone field in a model.")
|
||
mobile_blacklisted = fields.Boolean(
|
||
string='Blacklisted Phone Is Mobile', compute="_compute_blacklisted", compute_sudo=True, store=False, groups="base.group_user",
|
||
help="Indicates if a blacklisted sanitized phone number is a mobile number. Helps distinguish which number is blacklisted \
|
||
when there is both a mobile and phone field in a model.")
|
||
phone_mobile_search = fields.Char("Phone/Mobile", store=False, search='_search_phone_mobile_search')
|
||
|
||
def _search_phone_mobile_search(self, operator, value):
|
||
value = value.strip() if isinstance(value, str) else value
|
||
phone_fields = [
|
||
fname for fname in self._phone_get_number_fields()
|
||
if fname in self._fields and self._fields[fname].store
|
||
]
|
||
if not phone_fields:
|
||
raise UserError(_('Missing definition of phone fields.'))
|
||
|
||
# search if phone/mobile is set or not
|
||
if (value is True or not value) and operator in ('=', '!='):
|
||
if value:
|
||
# inverse the operator
|
||
operator = '=' if operator == '!=' else '!='
|
||
op = expression.AND if operator == '=' else expression.OR
|
||
return op([[(phone_field, operator, False)] for phone_field in phone_fields])
|
||
|
||
if self._phone_search_min_length and len(value) < self._phone_search_min_length:
|
||
raise UserError(_('Please enter at least 3 characters when searching a Phone/Mobile number.'))
|
||
|
||
pattern = r'[\s\\./\(\)\-]'
|
||
sql_operator = {'=like': 'LIKE', '=ilike': 'ILIKE'}.get(operator, operator)
|
||
|
||
if value.startswith('+') or value.startswith('00'):
|
||
if operator in expression.NEGATIVE_TERM_OPERATORS:
|
||
# searching on +32485112233 should also finds 0032485112233 (and vice versa)
|
||
# we therefore remove it from input value and search for both of them in db
|
||
where_str = ' AND '.join(
|
||
f"""model.{phone_field} IS NULL OR (
|
||
REGEXP_REPLACE(model.{phone_field}, %s, '', 'g') {sql_operator} %s OR
|
||
REGEXP_REPLACE(model.{phone_field}, %s, '', 'g') {sql_operator} %s
|
||
)"""
|
||
for phone_field in phone_fields
|
||
)
|
||
else:
|
||
# searching on +32485112233 should also finds 0032485112233 (and vice versa)
|
||
# we therefore remove it from input value and search for both of them in db
|
||
where_str = ' OR '.join(
|
||
f"""model.{phone_field} IS NOT NULL AND (
|
||
REGEXP_REPLACE(model.{phone_field}, %s, '', 'g') {sql_operator} %s OR
|
||
REGEXP_REPLACE(model.{phone_field}, %s, '', 'g') {sql_operator} %s
|
||
)"""
|
||
for phone_field in phone_fields
|
||
)
|
||
query = f"SELECT model.id FROM {self._table} model WHERE {where_str};"
|
||
|
||
term = re.sub(pattern, '', value[1 if value.startswith('+') else 2:])
|
||
if operator not in ('=', '!='): # for like operators
|
||
term = f'{term}%'
|
||
self._cr.execute(
|
||
query, (pattern, '00' + term, pattern, '+' + term) * len(phone_fields)
|
||
)
|
||
else:
|
||
if operator in expression.NEGATIVE_TERM_OPERATORS:
|
||
where_str = ' AND '.join(
|
||
f"(model.{phone_field} IS NULL OR REGEXP_REPLACE(model.{phone_field}, %s, '', 'g') {sql_operator} %s)"
|
||
for phone_field in phone_fields
|
||
)
|
||
else:
|
||
where_str = ' OR '.join(
|
||
f"(model.{phone_field} IS NOT NULL AND REGEXP_REPLACE(model.{phone_field}, %s, '', 'g') {sql_operator} %s)"
|
||
for phone_field in phone_fields
|
||
)
|
||
query = f"SELECT model.id FROM {self._table} model WHERE {where_str};"
|
||
term = re.sub(pattern, '', value)
|
||
if operator not in ('=', '!='): # for like operators
|
||
term = f'%{term}%'
|
||
self._cr.execute(query, (pattern, term) * len(phone_fields))
|
||
res = self._cr.fetchall()
|
||
if not res:
|
||
return [(0, '=', 1)]
|
||
return [('id', 'in', [r[0] for r in res])]
|
||
|
||
@api.depends(lambda self: self._phone_get_sanitize_triggers())
|
||
def _compute_phone_sanitized(self):
|
||
self._assert_phone_field()
|
||
number_fields = self._phone_get_number_fields()
|
||
for record in self:
|
||
for fname in number_fields:
|
||
sanitized = record._phone_format(fname=fname)
|
||
if sanitized:
|
||
break
|
||
record.phone_sanitized = sanitized
|
||
|
||
@api.depends('phone_sanitized')
|
||
def _compute_blacklisted(self):
|
||
# TODO : Should remove the sudo as compute_sudo defined on methods.
|
||
# But if user doesn't have access to mail.blacklist, doen't work without sudo().
|
||
blacklist = set(self.env['phone.blacklist'].sudo().search([
|
||
('number', 'in', self.mapped('phone_sanitized'))]).mapped('number'))
|
||
number_fields = self._phone_get_number_fields()
|
||
for record in self:
|
||
record.phone_sanitized_blacklisted = record.phone_sanitized in blacklist
|
||
mobile_blacklisted = phone_blacklisted = False
|
||
# This is a bit of a hack. Assume that any "mobile" numbers will have the word 'mobile'
|
||
# in them due to varying field names and assume all others are just "phone" numbers.
|
||
# Note that the limitation of only having 1 phone_sanitized value means that a phone/mobile number
|
||
# 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_format(fname=number_field) == record.phone_sanitized
|
||
else:
|
||
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
|
||
|
||
@api.model
|
||
def _search_phone_sanitized_blacklisted(self, operator, value):
|
||
# Assumes operator is '=' or '!=' and value is True or False
|
||
self._assert_phone_field()
|
||
if operator != '=':
|
||
if operator == '!=' and isinstance(value, bool):
|
||
value = not value
|
||
else:
|
||
raise NotImplementedError()
|
||
|
||
if value:
|
||
query = """
|
||
SELECT m.id
|
||
FROM phone_blacklist bl
|
||
JOIN %s m
|
||
ON m.phone_sanitized = bl.number AND bl.active
|
||
"""
|
||
else:
|
||
query = """
|
||
SELECT m.id
|
||
FROM %s m
|
||
LEFT JOIN phone_blacklist bl
|
||
ON m.phone_sanitized = bl.number AND bl.active
|
||
WHERE bl.id IS NULL
|
||
"""
|
||
self._cr.execute(query % self._table)
|
||
res = self._cr.fetchall()
|
||
if not res:
|
||
return [(0, '=', 1)]
|
||
return [('id', 'in', [r[0] for r in res])]
|
||
|
||
def _assert_phone_field(self):
|
||
if not hasattr(self, "_phone_get_number_fields"):
|
||
raise UserError(_('Invalid primary phone field on model %s', self._name))
|
||
if not any(fname in self and self._fields[fname].type == 'char' for fname in self._phone_get_number_fields()):
|
||
raise UserError(_('Invalid primary phone field on model %s', self._name))
|
||
|
||
def _phone_get_sanitize_triggers(self):
|
||
""" Tool method to get all triggers for sanitize """
|
||
res = [self._phone_get_country_field()] if self._phone_get_country_field() else []
|
||
return res + self._phone_get_number_fields()
|
||
|
||
def _phone_set_blacklisted(self):
|
||
return self.env['phone.blacklist'].sudo()._add([r.phone_sanitized for r in self])
|
||
|
||
def _phone_reset_blacklisted(self):
|
||
return self.env['phone.blacklist'].sudo()._remove([r.phone_sanitized for r in self])
|
||
|
||
def phone_action_blacklist_remove(self):
|
||
# wizard access rights currently not working as expected and allows users without access to
|
||
# open this wizard, therefore we check to make sure they have access before the wizard opens.
|
||
can_access = self.env['phone.blacklist'].check_access_rights('write', raise_exception=False)
|
||
if can_access:
|
||
return {
|
||
'name': 'Are you sure you want to unblacklist this Phone Number?',
|
||
'type': 'ir.actions.act_window',
|
||
'view_mode': 'form',
|
||
'res_model': 'phone.blacklist.remove',
|
||
'target': 'new',
|
||
}
|
||
else:
|
||
raise AccessError("You do not have the access right to unblacklist phone numbers. Please contact your administrator.")
|