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 _compute_sanitized_numbers(self): for composer in self: 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'] ] if invalid_numbers: raise UserError( _('Following numbers are not correctly encoded: %s') % repr(invalid_numbers)) composer.sanitized_numbers = ','.join(sanitized_numbers) else: composer.sanitized_numbers = False
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 _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 action_send_sms(self): self.ensure_one() numbers = [number.strip() for number in self.numbers.split(',')] 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'] ] if invalid_numbers: raise exceptions.UserError( _('Following numbers are not correctly encoded: %s, example : "+32 495 85 85 77, +33 545 55 55 55"' ) % repr(invalid_numbers)) self.env['sms.api']._send_sms_batch([{ 'res_id': 0, 'number': number, 'content': self.mailing_id.body_plaintext, } for number in sanitized_numbers]) return True
def _compute_recipient_single_valid(self): for composer in self: 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) else: composer.recipient_single_valid = False
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 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 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 valid_alternative_9number(info): """Validar um numero alternativo adicionando um numero 9""" if not info.get("number"): return False alternative_number = get_number_e164(info.get("number")) valid_number = phone_validation.phone_sanitize_numbers_w_record( [alternative_number], info.get("partner")) if valid_number.get(alternative_number).get("sanitized"): return valid_number.get(alternative_number).get("sanitized") return 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']
def action_send_sms(self): 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']] record = self.env[self.mailing_id.mailing_model_real].search([], limit=1) body = self.mailing_id.body_plaintext if record: # Returns a proper error if there is a syntax error with jinja body = self.env['mail.render.mixin']._render_template(body, self.mailing_id.mailing_model_real, record.ids)[record.id] # res_id is used to map the result to the number to log notifications as IAP does not return numbers... # TODO: clean IAP to make it return a clean dict with numbers / use custom keys / rename res_id to external_id sent_sms_list = self.env['sms.api']._send_sms_batch([{ 'res_id': number, 'number': number, 'content': body, } for number in sanitized_numbers]) error_messages = {} if any(sent_sms.get('state') != 'success' for sent_sms in sent_sms_list): error_messages = self.env['sms.api']._get_sms_api_error_messages() notification_messages = [] if invalid_numbers: notification_messages.append(_('The following numbers are not correctly encoded: %s', ', '.join(invalid_numbers))) for sent_sms in sent_sms_list: if sent_sms.get('state') == 'success': notification_messages.append( _('Test SMS successfully sent to %s', sent_sms.get('res_id'))) elif sent_sms.get('state'): notification_messages.append( _('Test SMS could not be sent to %s:<br>%s', sent_sms.get('res_id'), error_messages.get(sent_sms['state'], _("An error occurred."))) ) if notification_messages: self.mailing_id._message_log(body='<ul>%s</ul>' % ''.join( ['<li>%s</li>' % notification_message for notification_message in notification_messages] )) return True
def _compute_sanitized_numbers(self): if self.numbers: numbers = [number.strip() for number in self.numbers.split(',')] 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'] ] if invalid_numbers: raise exceptions.UserError( _('Following numbers are not correctly encoded: %s') % repr(invalid_numbers)) self.sanitized_numbers = ','.join(sanitized_numbers) else: self.sanitized_numbers = False
def action_send_sms(self): self.ensure_one() numbers = [number.strip() for number in self.numbers.split(',')] 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']] if invalid_numbers: raise exceptions.UserError(_('Following numbers are not correctly encoded: %s, example : "+32 495 85 85 77, +33 545 55 55 55"', repr(invalid_numbers))) record = self.env[self.mailing_id.mailing_model_real].search([], limit=1) body = self.mailing_id.body_plaintext if record: # Returns a proper error if there is a syntax error with jinja body = self.env['mail.render.mixin']._render_template(body, self.mailing_id.mailing_model_real, record.ids)[record.id] self.env['sms.api']._send_sms_batch([{ 'res_id': 0, 'number': number, 'content': body, } for number in sanitized_numbers]) return True
def phone_get_sanitized_number(self, number_fname='mobile', force_format='E164'): sanitized = super(PhoneMixin, self).phone_get_sanitized_number( number_fname='mobile', force_format='E164') if sanitized: return sanitized country_fname = self._phone_get_country_field() number = self[number_fname] # Valida numero alternativo adicionando um 9 e caracteres de controle alternative_number = helpers.get_number_e164(number) sanitized = phone_validation.phone_sanitize_numbers_w_record( [alternative_number], self, record_country_fname=country_fname, force_format=force_format)[alternative_number]['sanitized'] if sanitized: return number return False
def _sms_get_recipients_info(self, force_field=False, partner_fallback=True): """" Get SMS recipient information on current record set. This method checks for numbers and sanitation in order to centralize computation. Example of use cases * click on a field -> number is actually forced from field, find customer linked to record, force its number to field or fallback on customer fields; * contact -> find numbers from all possible phone fields on record, find customer, force its number to found field number or fallback on customer fields; :param force_field: either give a specific field to find phone number, either generic heuristic is used to find one based on ``_sms_get_number_fields``; :param partner_fallback: if no value found in the record, check its customer values based on ``_sms_get_default_partners``; :return dict: record.id: { 'partner': a res.partner recordset that is the customer (void or singleton) linked to the recipient. See ``_sms_get_default_partners``; 'sanitized': sanitized number to use (coming from record's field or partner's phone fields). Set to False is number impossible to parse and format; 'number': original number before sanitation; 'partner_store': whether the number comes from the customer phone fields. If False it means number comes from the record itself, even if linked to a customer; 'field_store': field in which the number has been found (generally mobile or phone, see ``_sms_get_number_fields``); } for each record in self """ result = dict.fromkeys(self.ids, False) tocheck_fields = [force_field ] if force_field else self._sms_get_number_fields() for record in self: all_numbers = [ record[fname] for fname in tocheck_fields if fname in record ] all_partners = record._sms_get_default_partners() 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'] if valid_number: break if valid_number: result[record.id] = { 'partner': all_partners[0] if all_partners else self.env['res.partner'], 'sanitized': valid_number, 'number': record[fname], 'partner_store': False, 'field_store': fname, } elif all_partners and partner_fallback: partner = self.env['res.partner'] for partner in all_partners: for fname in self.env[ 'res.partner']._sms_get_number_fields(): valid_number = phone_validation.phone_sanitize_numbers_w_record( [partner[fname]], record)[partner[fname]]['sanitized'] if valid_number: break if not valid_number: fname = 'mobile' if partner.mobile else ( 'phone' if partner.phone else 'mobile') result[record.id] = { 'partner': partner, 'sanitized': valid_number if valid_number else False, 'number': partner[fname], 'partner_store': True, 'field_store': fname, } else: # did not find any sanitized number -> take first set value as fallback; # if none, just assign False to the first available number field value, fname = next( ((value, fname) for value, fname in zip(all_numbers, tocheck_fields) if value), (False, tocheck_fields[0] if tocheck_fields else False)) result[record.id] = { 'partner': self.env['res.partner'], 'sanitized': False, 'number': value, 'partner_store': False, 'field_store': fname } return result
def _notify_record_by_sms(self, message, recipients_data, msg_vals=False, sms_numbers=None, sms_pid_to_number=None, check_existing=False, put_in_queue=False, **kwargs): """ Notification method: by SMS. :param message: mail.message record to notify; :param recipients_data: see ``_notify_thread``; :param msg_vals: see ``_notify_thread``; :param sms_numbers: additional numbers to notify in addition to partners and classic recipients; :param pid_to_number: force a number to notify for a given partner ID instead of taking its mobile / phone number; :param check_existing: check for existing notifications to update based on mailed recipient, otherwise create new notifications; :param put_in_queue: use cron to send queued SMS instead of sending them directly; """ sms_pid_to_number = sms_pid_to_number if sms_pid_to_number is not None else {} sms_numbers = sms_numbers if sms_numbers is not None else [] sms_create_vals = [] sms_all = self.env['sms.sms'].sudo() # pre-compute SMS data body = msg_vals['body'] if msg_vals and msg_vals.get( 'body') else message.body sms_base_vals = { 'body': html2plaintext(body), 'mail_message_id': message.id, 'state': 'outgoing', } # notify from computed recipients_data (followers, specific recipients) partners_data = [ r for r in recipients_data['partners'] if r['notif'] == 'sms' ] partner_ids = [r['id'] for r in partners_data] 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)) # 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() ] sms_create_vals += [ dict( sms_base_vals, partner_id=False, number=n, state='outgoing' if n else 'error', error_code='' if n else 'sms_number_missing', ) for n in tocreate_numbers ] # create sms and notification existing_pids, existing_numbers = [], [] if sms_create_vals: sms_all |= self.env['sms.sms'].sudo().create(sms_create_vals) if check_existing: existing = self.env['mail.notification'].sudo().search([ '|', ('res_partner_id', 'in', partner_ids), '&', ('res_partner_id', '=', False), ('sms_number', 'in', sms_numbers), ('notification_type', '=', 'sms'), ('mail_message_id', '=', message.id) ]) for n in existing: if n.res_partner_id.id in partner_ids and n.mail_message_id == message: existing_pids.append(n.res_partner_id.id) if not n.res_partner_id and n.sms_number in sms_numbers and n.mail_message_id == message: existing_numbers.append(n.sms_number) notif_create_values = [ { 'mail_message_id': message.id, 'res_partner_id': sms.partner_id.id, 'sms_number': sms.number, 'notification_type': 'sms', 'sms_id': sms.id, 'is_read': True, # discard Inbox notification 'notification_status': 'ready' if sms.state == 'outgoing' else 'exception', 'failure_type': '' if sms.state == 'outgoing' else sms.error_code, } for sms in sms_all if (sms.partner_id and sms.partner_id.id not in existing_pids) or (not sms.partner_id and sms.number not in existing_numbers) ] if notif_create_values: self.env['mail.notification'].sudo().create( notif_create_values) if existing_pids or existing_numbers: for sms in sms_all: notif = next( (n for n in existing if (n.res_partner_id.id in existing_pids and n.res_partner_id.id == sms.partner_id.id) or (not n.res_partner_id and n.sms_number in existing_numbers and n.sms_number == sms.number)), False) if notif: notif.write({ 'notification_type': 'sms', 'notification_status': 'ready', 'sms_id': sms.id, 'sms_number': sms.number, }) if sms_all and not put_in_queue: sms_all.filtered(lambda sms: sms.state == 'outgoing').send( auto_commit=False, raise_exception=False) return True
def add(self, number): sanitized = phone_validation.phone_sanitize_numbers_w_record( [number], self.env.user)[number]['sanitized'] return self._add([sanitized])
def _sms_get_recipients_info(self, force_field=False): """" Get SMS recipient information on current record set. This method checks for numbers and sanitation in order to centralize computation. Example of use cases * click on a field -> number is actually forced from field, find customer linked to record, force its number to field or fallback on customer fields; * contact -> find numbers from all possible phone fields on record, find customer, force its number to found field number or fallback on customer fields; :return dict: record.id: { 'partner': a res.partner recordset that is the customer (void or singleton); 'sanitized': sanitized number to use (coming from record's field or partner's mobile or phone). Set to False is number impossible to parse and format; 'number': original number before sanitation; } for each record in self """ result = dict.fromkeys(self.ids, False) number_fields = self._sms_get_number_fields() for record in self: tocheck_fields = [force_field] if force_field else number_fields all_numbers = [ record[fname] for fname in tocheck_fields if fname in record ] all_partners = record._sms_get_default_partners() 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'] if valid_number: break if valid_number: result[record.id] = { 'partner': all_partners[0] if all_partners else self.env['res.partner'], 'sanitized': valid_number, 'number': valid_number, } elif all_partners: partner_number, partner = False, self.env['res.partner'] for partner in all_partners: partner_number = partner.mobile or partner.phone if partner_number: partner_number = phone_validation.phone_sanitize_numbers_w_record( [partner_number], record)[partner_number]['sanitized'] if partner_number: break if partner_number: result[record.id] = { 'partner': partner, 'sanitized': partner_number, 'number': partner_number } else: result[record.id] = { 'partner': partner, 'sanitized': False, 'number': partner.mobile or partner.phone } elif all_numbers: result[record.id] = { 'partner': self.env['res.partner'], 'sanitized': False, 'number': all_numbers[0] } else: result[record.id] = { 'partner': self.env['res.partner'], 'sanitized': False, 'number': False } return result
def _notify_thread_by_sms(self, message, recipients_data, msg_vals=False, sms_numbers=None, sms_pid_to_number=None, check_existing=False, put_in_queue=False, **kwargs): """ Notification method: by SMS. :param message: ``mail.message`` record to notify; :param recipients_data: list of recipients information (based on res.partner records), formatted like [{'active': partner.active; 'id': id of the res.partner being recipient to notify; 'groups': res.group IDs if linked to a user; 'notif': 'inbox', 'email', 'sms' (SMS App); 'share': partner.partner_share; 'type': 'customer', 'portal', 'user;' }, {...}]. See ``MailThread._notify_get_recipients``; :param msg_vals: dictionary of values used to create the message. If given it may be used to access values related to ``message`` without accessing it directly. It lessens query count in some optimized use cases by avoiding access message content in db; :param sms_numbers: additional numbers to notify in addition to partners and classic recipients; :param pid_to_number: force a number to notify for a given partner ID instead of taking its mobile / phone number; :param check_existing: check for existing notifications to update based on mailed recipient, otherwise create new notifications; :param put_in_queue: use cron to send queued SMS instead of sending them directly; """ sms_pid_to_number = sms_pid_to_number if sms_pid_to_number is not None else {} sms_numbers = sms_numbers if sms_numbers is not None else [] sms_create_vals = [] sms_all = self.env['sms.sms'].sudo() # pre-compute SMS data body = msg_vals[ 'body'] if msg_vals and 'body' in msg_vals else message.body sms_base_vals = { 'body': html2plaintext(body), 'mail_message_id': message.id, 'state': 'outgoing', } # notify from computed recipients_data (followers, specific recipients) partners_data = [r for r in recipients_data if r['notif'] == 'sms'] partner_ids = [r['id'] for r in partners_data] 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)) # 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() ] sms_create_vals += [ dict( sms_base_vals, partner_id=False, number=n, state='outgoing' if n else 'error', failure_type='' if n else 'sms_number_missing', ) for n in tocreate_numbers ] # create sms and notification existing_pids, existing_numbers = [], [] if sms_create_vals: sms_all |= self.env['sms.sms'].sudo().create(sms_create_vals) if check_existing: existing = self.env['mail.notification'].sudo().search([ '|', ('res_partner_id', 'in', partner_ids), '&', ('res_partner_id', '=', False), ('sms_number', 'in', sms_numbers), ('notification_type', '=', 'sms'), ('mail_message_id', '=', message.id) ]) for n in existing: if n.res_partner_id.id in partner_ids and n.mail_message_id == message: existing_pids.append(n.res_partner_id.id) if not n.res_partner_id and n.sms_number in sms_numbers and n.mail_message_id == message: existing_numbers.append(n.sms_number) notif_create_values = [ { 'author_id': message.author_id.id, 'mail_message_id': message.id, 'res_partner_id': sms.partner_id.id, 'sms_number': sms.number, 'notification_type': 'sms', 'sms_id': sms.id, 'is_read': True, # discard Inbox notification 'notification_status': 'ready' if sms.state == 'outgoing' else 'exception', 'failure_type': '' if sms.state == 'outgoing' else sms.failure_type, } for sms in sms_all if (sms.partner_id and sms.partner_id.id not in existing_pids) or (not sms.partner_id and sms.number not in existing_numbers) ] if notif_create_values: self.env['mail.notification'].sudo().create( notif_create_values) if existing_pids or existing_numbers: for sms in sms_all: notif = next( (n for n in existing if (n.res_partner_id.id in existing_pids and n.res_partner_id.id == sms.partner_id.id) or (not n.res_partner_id and n.sms_number in existing_numbers and n.sms_number == sms.number)), False) if notif: notif.write({ 'notification_type': 'sms', 'notification_status': 'ready', 'sms_id': sms.id, 'sms_number': sms.number, }) if sms_all and not put_in_queue: sms_all.filtered(lambda sms: sms.state == 'outgoing').send( auto_commit=False, raise_exception=False) return True