mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-22 09:42:05 +02:00
19.0 vanilla
This commit is contained in:
parent
5df8c07b59
commit
daa394e8b0
2114 changed files with 564841 additions and 299642 deletions
|
|
@ -1,66 +1,117 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from markupsafe import Markup
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
from odoo.tools.urls import urljoin as url_join
|
||||
|
||||
|
||||
class MassSMSTest(models.TransientModel):
|
||||
class MailingSmsTest(models.TransientModel):
|
||||
_name = 'mailing.sms.test'
|
||||
_description = 'Test SMS Mailing'
|
||||
|
||||
def _default_numbers(self):
|
||||
return self.env.user.partner_id.phone_sanitized or ""
|
||||
previous_numbers = self.env['mailing.sms.test'].search([('create_uid', '=', self.env.uid)], order='create_date desc', limit=1).numbers
|
||||
return previous_numbers or self.env.user.partner_id.phone_sanitized or ""
|
||||
|
||||
numbers = fields.Text(string='Number(s)', required=True,
|
||||
default=_default_numbers, help='Carriage-return-separated list of phone numbers')
|
||||
mailing_id = fields.Many2one('mailing.mailing', string='Mailing', required=True, ondelete='cascade')
|
||||
|
||||
def _prepare_test_trace_values(self, record, sms_number, sms_uuid, body):
|
||||
trace_code = self.env['mailing.trace']._get_random_code()
|
||||
trace_values = {
|
||||
'is_test_trace': True,
|
||||
'mass_mailing_id': self.mailing_id.id,
|
||||
'model': record._name,
|
||||
'res_id': record.id,
|
||||
'sms_code': trace_code,
|
||||
'sms_number': sms_number,
|
||||
'sms_tracker_ids': [(0, 0, {'sms_uuid': sms_uuid})],
|
||||
'trace_type': 'sms',
|
||||
}
|
||||
unsubscribe_info = self.env['sms.composer']._get_unsubscribe_info(
|
||||
self.env['sms.composer']._get_unsubscribe_url(self.mailing_id.id, trace_code)
|
||||
)
|
||||
body = '%s\n%s' % (body or '', unsubscribe_info)
|
||||
return trace_values, body
|
||||
|
||||
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']]
|
||||
sanitized_numbers = [self.env.user._phone_format(number=number) for number in numbers]
|
||||
valid_numbers = [number for sanitized, number in zip(sanitized_numbers, numbers) if sanitized]
|
||||
invalid_numbers = [number for sanitized, number in zip(sanitized_numbers, numbers) if not sanitized]
|
||||
|
||||
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 qweb
|
||||
body = self.env['mail.render.mixin']._render_template(body, self.mailing_id.mailing_model_real, record.ids)[record.id]
|
||||
# create and send SMS for valid numbers (skip other, likely to crash)
|
||||
sms_values_list, trace_values_list = [], []
|
||||
for sanitized_number in valid_numbers:
|
||||
if not sanitized_number:
|
||||
continue
|
||||
sms_values = {
|
||||
'number': sanitized_number,
|
||||
'uuid': uuid4().hex,
|
||||
'state': 'outgoing',
|
||||
}
|
||||
# include unsubscribe link and generate fake trace to test unsubscribe
|
||||
# flow if sms_allow_unsubscribe is enabled
|
||||
sms_body = body
|
||||
if self.mailing_id.sms_allow_unsubscribe:
|
||||
trace_values, sms_body = self._prepare_test_trace_values(record, sanitized_number, sms_values['uuid'], sms_body)
|
||||
trace_values_list.append(trace_values)
|
||||
sms_values['body'] = sms_body
|
||||
sms_values_list.append(sms_values)
|
||||
sms_sudo = self.env['sms.sms'].sudo().create(sms_values_list)
|
||||
if trace_values_list:
|
||||
self.env['mailing.trace'].create(trace_values_list)
|
||||
|
||||
# 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()
|
||||
sms_api = self.env.company._get_sms_api_class()(self.env)
|
||||
sent_sms_list = sms_api._send_sms_batch(
|
||||
[{
|
||||
'content': body,
|
||||
'numbers': [{'number': sms.number, 'uuid': sms.uuid} for sms in sms_sudo],
|
||||
}],
|
||||
delivery_reports_url=url_join(self[0].get_base_url(), '/sms/status')
|
||||
)
|
||||
|
||||
notification_messages = []
|
||||
if invalid_numbers:
|
||||
notification_messages.append(_('The following numbers are not correctly encoded: %s',
|
||||
', '.join(invalid_numbers)))
|
||||
|
||||
sms_uuid_to_number_map = {sms.uuid: sms.number for sms in sms_sudo}
|
||||
for sent_sms in sent_sms_list:
|
||||
if sent_sms.get('state') == 'success':
|
||||
recipient = sms_uuid_to_number_map.get(sent_sms.get('uuid'))
|
||||
# 'success' and 'sent' IAP/Twilio both resolve to 'pending' SMS state
|
||||
# (= send for Odoo) via IAP_TO_SMS_STATE_SUCCESS
|
||||
if sent_sms.get('state') in ('success', 'sent'):
|
||||
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.")))
|
||||
_('Test SMS successfully sent to %s', recipient)
|
||||
)
|
||||
else:
|
||||
failure_explanation = sms_api._get_sms_api_error_messages().get(sent_sms['state'])
|
||||
failure_reason = sent_sms.get('failure_reason')
|
||||
notification_messages.append(_(
|
||||
"Test SMS could not be sent to %(destination)s: %(state)s",
|
||||
destination=recipient,
|
||||
state=failure_explanation or failure_reason or _("An error occurred."),
|
||||
))
|
||||
if invalid_numbers:
|
||||
notification_messages.append(
|
||||
_(
|
||||
"Test SMS skipped those numbers as they appear invalid: %(numbers)s",
|
||||
numbers=', '.join(invalid_numbers)
|
||||
)
|
||||
)
|
||||
|
||||
if notification_messages:
|
||||
self.mailing_id._message_log(body='<ul>%s</ul>' % ''.join(
|
||||
['<li>%s</li>' % notification_message for notification_message in notification_messages]
|
||||
))
|
||||
message_body = Markup(
|
||||
f"<ul>{''.join('<li>%s</li>' for _ in notification_messages)}</ul>"
|
||||
) % tuple(notification_messages)
|
||||
self.mailing_id._message_log(body=message_body)
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -6,22 +6,22 @@
|
|||
<field name="arch" type="xml">
|
||||
<form string="Send a Sample SMS">
|
||||
<p class="text-muted">
|
||||
Send a sample SMS for testing purpose to the numbers below (carriage-return-separated list).
|
||||
Send a sample SMS for testing purpose to the numbers below.
|
||||
</p>
|
||||
<group>
|
||||
<field name="numbers" placeholder="+32 495 85 85 77 +33 545 55 55 55"/>
|
||||
<field name="mailing_id" invisible="1"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Send" name="action_send_sms" type="object" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Cancel" class="btn btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
<button string="Send Test" name="action_send_sms" type="object" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Discard" class="btn btn-secondary" special="cancel" data-hotkey="x"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mailing_sms_test_action" model="ir.actions.act_window">
|
||||
<field name="name">Test SMS Marketing</field>
|
||||
<field name="name">Test Mailing</field>
|
||||
<field name="res_model">mailing.sms.test</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import werkzeug.urls
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo import _, api, Command, fields, models, tools
|
||||
|
||||
|
||||
class SMSComposer(models.TransientModel):
|
||||
class SmsComposer(models.TransientModel):
|
||||
_inherit = 'sms.composer'
|
||||
|
||||
# mass mode with mass sms
|
||||
|
|
@ -18,25 +16,26 @@ class SMSComposer(models.TransientModel):
|
|||
# Mass mode specific
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _get_unsubscribe_url(self, res_id, trace_code, number):
|
||||
return werkzeug.urls.url_join(
|
||||
def _get_unsubscribe_url(self, mailing_id, trace_code):
|
||||
return tools.urls.urljoin(
|
||||
self.get_base_url(),
|
||||
'/sms/%s/%s' % (self.mailing_id.id, trace_code)
|
||||
'/sms/%s/%s' % (mailing_id, trace_code)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_unsubscribe_info(self, url):
|
||||
return _('STOP SMS : %(unsubscribe_url)s', unsubscribe_url=url)
|
||||
return _('STOP SMS: %(unsubscribe_url)s', unsubscribe_url=url)
|
||||
|
||||
def _prepare_mass_sms_trace_values(self, record, sms_values):
|
||||
trace_code = self.env['mailing.trace']._get_random_code()
|
||||
trace_values = {
|
||||
'mass_mailing_id': self.mailing_id.id,
|
||||
'model': self.res_model,
|
||||
'res_id': record.id,
|
||||
'trace_type': 'sms',
|
||||
'mass_mailing_id': self.mailing_id.id,
|
||||
'sms_number': sms_values['number'],
|
||||
'sms_code': trace_code,
|
||||
'sms_number': sms_values['number'],
|
||||
'sms_tracker_ids': [Command.create({'sms_uuid': sms_values['uuid']})],
|
||||
'trace_type': 'sms',
|
||||
}
|
||||
if sms_values['state'] == 'error':
|
||||
trace_values['failure_type'] = sms_values['failure_type']
|
||||
|
|
@ -46,13 +45,13 @@ class SMSComposer(models.TransientModel):
|
|||
trace_values['trace_status'] = 'cancel'
|
||||
else:
|
||||
if self.mass_sms_allow_unsubscribe:
|
||||
stop_sms = self._get_unsubscribe_info(self._get_unsubscribe_url(record.id, trace_code, sms_values['number']))
|
||||
stop_sms = self._get_unsubscribe_info(self._get_unsubscribe_url(self.mailing_id.id, trace_code))
|
||||
sms_values['body'] = '%s\n%s' % (sms_values['body'] or '', stop_sms)
|
||||
return trace_values
|
||||
|
||||
def _get_optout_record_ids(self, records, recipients_info):
|
||||
""" Fetch opt-out records based on mailing. """
|
||||
res = super(SMSComposer, self)._get_optout_record_ids(records, recipients_info)
|
||||
res = super()._get_optout_record_ids(records, recipients_info)
|
||||
if self.mailing_id:
|
||||
optout_res_ids = self.mailing_id._get_opt_out_list_sms()
|
||||
res += optout_res_ids
|
||||
|
|
@ -60,14 +59,14 @@ class SMSComposer(models.TransientModel):
|
|||
|
||||
def _get_done_record_ids(self, records, recipients_info):
|
||||
""" A/B testing could lead to records having been already mailed. """
|
||||
res = super(SMSComposer, self)._get_done_record_ids(records, recipients_info)
|
||||
res = super()._get_done_record_ids(records, recipients_info)
|
||||
if self.mailing_id:
|
||||
seen_ids, seen_list = self.mailing_id._get_seen_list_sms()
|
||||
res += seen_ids
|
||||
return res
|
||||
|
||||
def _prepare_body_values(self, records):
|
||||
all_bodies = super(SMSComposer, self)._prepare_body_values(records)
|
||||
all_bodies = super()._prepare_body_values(records)
|
||||
if self.mailing_id:
|
||||
tracker_values = self.mailing_id._get_link_tracker_values()
|
||||
for sms_id, body in all_bodies.items():
|
||||
|
|
@ -76,22 +75,47 @@ class SMSComposer(models.TransientModel):
|
|||
return all_bodies
|
||||
|
||||
def _prepare_mass_sms_values(self, records):
|
||||
result = super(SMSComposer, self)._prepare_mass_sms_values(records)
|
||||
if self.composition_mode == 'mass' and self.mailing_id:
|
||||
for record in records:
|
||||
sms_values = result[record.id]
|
||||
# In mass mailing only, add 'mailing.trace' values directly in the o2m field
|
||||
result = super()._prepare_mass_sms_values(records)
|
||||
if not self._is_mass_sms():
|
||||
return result
|
||||
|
||||
trace_values = self._prepare_mass_sms_trace_values(record, sms_values)
|
||||
sms_values.update({
|
||||
'mailing_id': self.mailing_id.id,
|
||||
'mailing_trace_ids': [(0, 0, trace_values)],
|
||||
})
|
||||
for record in records:
|
||||
sms_values = result[record.id]
|
||||
|
||||
trace_values = self._prepare_mass_sms_trace_values(record, sms_values)
|
||||
sms_values.update({
|
||||
'mailing_id': self.mailing_id.id,
|
||||
'mailing_trace_ids': [(0, 0, trace_values)],
|
||||
})
|
||||
return result
|
||||
|
||||
def _prepare_mass_sms(self, records, sms_record_values):
|
||||
sms_all = super(SMSComposer, self)._prepare_mass_sms(records, sms_record_values)
|
||||
sms_all = super()._prepare_mass_sms(records, sms_record_values)
|
||||
if self.mailing_id:
|
||||
updated_bodies = sms_all._update_body_short_links()
|
||||
for sms in sms_all:
|
||||
sms.body = updated_bodies[sms.id]
|
||||
return sms_all
|
||||
|
||||
def _is_mass_sms(self):
|
||||
return self.composition_mode == 'mass' and self.mailing_id
|
||||
|
||||
def _filter_out_and_handle_revoked_sms_values(self, sms_values_all):
|
||||
# Filter out canceled sms of mass mailing and create traces for canceled ones.
|
||||
results = super()._filter_out_and_handle_revoked_sms_values(sms_values_all)
|
||||
if not self._is_mass_sms():
|
||||
return results
|
||||
self.env['mailing.trace'].sudo().create([
|
||||
trace_commands[0][2]
|
||||
for mail_values in results.values()
|
||||
if mail_values.get('state') == 'canceled' and mail_values['mailing_trace_ids']
|
||||
for trace_commands in (mail_values['mailing_trace_ids'],)
|
||||
# Ensure it is a create command
|
||||
if len(trace_commands) == 1 and len(trace_commands[0]) == 3 and trace_commands[0][0] == 0
|
||||
])
|
||||
return {
|
||||
res_id: sms_values
|
||||
for res_id, sms_values in results.items()
|
||||
if sms_values.get('state') != 'canceled'
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue