summaryrefslogtreecommitdiff
path: root/addons/phone_validation/models
diff options
context:
space:
mode:
Diffstat (limited to 'addons/phone_validation/models')
-rw-r--r--addons/phone_validation/models/__init__.py4
-rw-r--r--addons/phone_validation/models/mail_thread_phone.py103
-rw-r--r--addons/phone_validation/models/phone_blacklist.py109
-rw-r--r--addons/phone_validation/models/phone_validation_mixin.py19
4 files changed, 234 insertions, 1 deletions
diff --git a/addons/phone_validation/models/__init__.py b/addons/phone_validation/models/__init__.py
index 85ea0597483..dfe42e557e3 100644
--- a/addons/phone_validation/models/__init__.py
+++ b/addons/phone_validation/models/__init__.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import res_company
+from . import phone_blacklist
from . import phone_validation_mixin
+from . import mail_thread_phone
+from . import res_company
diff --git a/addons/phone_validation/models/mail_thread_phone.py b/addons/phone_validation/models/mail_thread_phone.py
new file mode 100644
index 00000000000..3e166985f8e
--- /dev/null
+++ b/addons/phone_validation/models/mail_thread_phone.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+
+class PhoneMixin(models.AbstractModel):
+ """ Purpose of this mixin is to offer two services
+
+ * compute a sanitized phone number based on ´´_sms_get_number_fields´´.
+ It takes first sanitized value, trying each field returned by the
+ method (see ``MailThread._sms_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.validation.mixin']
+
+ 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_blacklisted = fields.Boolean(
+ string='Phone Blacklisted', compute="_compute_phone_blacklisted", compute_sudo=True, store=False,
+ search="_search_phone_blacklisted", groups="base.group_user",
+ help="If the email address is on the blacklist, the contact won't receive mass mailing anymore, from any list")
+
+ @api.depends(lambda self: self._phone_get_number_fields())
+ 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_get_sanitized_number(number_fname=fname)
+ if sanitized:
+ break
+ record.phone_sanitized = sanitized
+
+ @api.depends('phone_sanitized')
+ def _compute_phone_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'))
+ for record in self:
+ record.phone_blacklisted = record.phone_sanitized in blacklist
+
+ @api.model
+ def _search_phone_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_number_fields(self):
+ """ This method returns the fields to use to find the number to use to
+ send an SMS on a record. """
+ return []
+
+ 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])
diff --git a/addons/phone_validation/models/phone_blacklist.py b/addons/phone_validation/models/phone_blacklist.py
new file mode 100644
index 00000000000..2a6e90b6cb5
--- /dev/null
+++ b/addons/phone_validation/models/phone_blacklist.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+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__)
+
+
+class PhoneBlackList(models.Model):
+ """ Blacklist of phone numbers. Used to avoid sending unwanted messages to people. """
+ _name = 'phone.blacklist'
+ _inherit = ['mail.thread']
+ _description = 'Phone Blacklist'
+ _rec_name = 'number'
+
+ number = fields.Char(string='Phone Number', required=True, index=True, tracking=True, help='Number should be E164 formatted')
+ active = fields.Boolean(default=True, tracking=True)
+
+ _sql_constraints = [
+ ('unique_number', 'unique (number)', 'Number already exists')
+ ]
+
+ @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)
+ to_create = []
+ done = set()
+ for value in values:
+ number = value['number']
+ sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized']
+ if not sanitized:
+ raise UserError(_('Invalid number %s') % number)
+ if sanitized in done:
+ continue
+ done.add(sanitized)
+ to_create.append(dict(value, number=sanitized))
+
+ """ To avoid crash during import due to unique email, return the existing records if any """
+ sql = '''SELECT number, id FROM phone_blacklist WHERE number = ANY(%s)'''
+ numbers = [v['number'] for v in to_create]
+ self._cr.execute(sql, (numbers,))
+ bl_entries = dict(self._cr.fetchall())
+ to_create = [v for v in to_create if v['number'] not in bl_entries]
+
+ results = super(PhoneBlackList, self).create(to_create)
+ return self.env['phone.blacklist'].browse(bl_entries.values()) | results
+
+ def write(self, values):
+ if 'number' in values:
+ number = values['number']
+ sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized']
+ if not sanitized:
+ raise UserError(_('Invalid number %s') % number)
+ values['number'] = sanitized
+ return super(PhoneBlackList, self).write(values)
+
+ def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
+ """ Override _search in order to grep search on sanitized number field """
+ if args:
+ new_args = []
+ for arg in args:
+ 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']
+ if sanitized:
+ new_args.append([arg[0], arg[1], sanitized])
+ else:
+ new_args.append(arg)
+ else:
+ new_args.append(arg)
+ else:
+ new_args = args
+ return super(PhoneBlackList, self)._search(new_args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
+
+ def add(self, number):
+ sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized']
+ return self._add([sanitized])
+
+ def _add(self, numbers):
+ """ Add or re activate a phone blacklist entry.
+
+ :param numbers: list of sanitized numbers """
+ records = self.env["phone.blacklist"].with_context(active_test=False).search([('number', 'in', numbers)])
+ todo = [n for n in numbers if n not in records.mapped('number')]
+ if records:
+ records.write({'active': True})
+ if todo:
+ records += self.create([{'number': n} for n in todo])
+ return records
+
+ def remove(self, number):
+ sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized']
+ return self._remove([sanitized])
+
+ def _remove(self, numbers):
+ """ Add de-activated or de-activate a phone blacklist entry.
+
+ :param numbers: list of sanitized numbers """
+ records = self.env["phone.blacklist"].with_context(active_test=False).search([('number', 'in', numbers)])
+ todo = [n for n in numbers if n not in records.mapped('number')]
+ if records:
+ records.write({'active': False})
+ if todo:
+ records += self.create([{'number': n, 'active': False} for n in todo])
+ return records
diff --git a/addons/phone_validation/models/phone_validation_mixin.py b/addons/phone_validation/models/phone_validation_mixin.py
index f062586c7c3..0e7ab1c2aaa 100644
--- a/addons/phone_validation/models/phone_validation_mixin.py
+++ b/addons/phone_validation/models/phone_validation_mixin.py
@@ -9,6 +9,11 @@ class PhoneValidationMixin(models.AbstractModel):
_name = 'phone.validation.mixin'
_description = 'Phone Validation Mixin'
+ def _phone_get_country_field(self):
+ if 'country_id' in self:
+ return 'country_id'
+ return False
+
def _phone_get_country(self):
if 'country_id' in self and self.country_id:
return self.country_id
@@ -31,3 +36,17 @@ class PhoneValidationMixin(models.AbstractModel):
force_format='INTERNATIONAL' if always_international else 'NATIONAL',
raise_exception=False
)
+
+ 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']