diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/phone_validation/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/phone_validation/models')
| -rw-r--r-- | addons/phone_validation/models/__init__.py | 7 | ||||
| -rw-r--r-- | addons/phone_validation/models/mail_thread_phone.py | 164 | ||||
| -rw-r--r-- | addons/phone_validation/models/phone_blacklist.py | 130 | ||||
| -rw-r--r-- | addons/phone_validation/models/phone_validation_mixin.py | 27 | ||||
| -rw-r--r-- | addons/phone_validation/models/res_partner.py | 19 |
5 files changed, 347 insertions, 0 deletions
diff --git a/addons/phone_validation/models/__init__.py b/addons/phone_validation/models/__init__.py new file mode 100644 index 00000000..1402c4c2 --- /dev/null +++ b/addons/phone_validation/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import phone_blacklist +from . import phone_validation_mixin +from . import mail_thread_phone +from . import res_partner 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 00000000..f81da543 --- /dev/null +++ b/addons/phone_validation/models/mail_thread_phone.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.addons.phone_validation.tools import phone_validation +from odoo.exceptions import AccessError, 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_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.") + + @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_get_sanitized_number(number_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_get_sanitized_number(number_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 + 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_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_get_country_field(self): + if 'country_id' in self: + return 'country_id' + return 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'] + + 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.") diff --git a/addons/phone_validation/models/phone_blacklist.py b/addons/phone_validation/models/phone_blacklist.py new file mode 100644 index 00000000..02506968 --- /dev/null +++ b/addons/phone_validation/models/phone_blacklist.py @@ -0,0 +1,130 @@ +# -*- 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_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: + 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_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.")) + 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.action_unarchive() + if todo: + records += self.create([{'number': n} for n in todo]) + return records + + def action_remove_with_reason(self, number, reason=None): + records = self.remove(number) + if reason: + for record in records: + record.message_post(body=_("Unblacklisting Reason: %s", reason)) + 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.action_archive() + if todo: + records += self.create([{'number': n, 'active': False} for n in todo]) + return records + + def phone_action_blacklist_remove(self): + 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', + } + + def action_add(self): + self.add(self.number) diff --git a/addons/phone_validation/models/phone_validation_mixin.py b/addons/phone_validation/models/phone_validation_mixin.py new file mode 100644 index 00000000..a430a537 --- /dev/null +++ b/addons/phone_validation/models/phone_validation_mixin.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# 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 PhoneValidationMixin(models.AbstractModel): + _name = 'phone.validation.mixin' + _description = 'Phone Validation Mixin' + + def _phone_get_country(self): + if 'country_id' in self and self.country_id: + return self.country_id + return self.env.company.country_id + + def phone_format(self, number, country=None, company=None): + country = country or self._phone_get_country() + if not country: + return number + return phone_validation.phone_format( + number, + country.code if country else None, + country.phone_code if country else None, + force_format='INTERNATIONAL', + raise_exception=False + ) diff --git a/addons/phone_validation/models/res_partner.py b/addons/phone_validation/models/res_partner.py new file mode 100644 index 00000000..d0b125e6 --- /dev/null +++ b/addons/phone_validation/models/res_partner.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class Partner(models.Model): + _name = 'res.partner' + _inherit = ['res.partner', 'phone.validation.mixin'] + + @api.onchange('phone', 'country_id', 'company_id') + def _onchange_phone_validation(self): + if self.phone: + self.phone = self.phone_format(self.phone) + + @api.onchange('mobile', 'country_id', 'company_id') + def _onchange_mobile_validation(self): + if self.mobile: + self.mobile = self.phone_format(self.mobile) |
