[MERGE] sms(*): allow direct sending and improve templates

PURPOSE

Sending SMS is sometimes required as an immediate marketing tool.
Using delayed crons is not always the best user choice. Implement a
Send Now mechanism in batch SMS.

Allow people to edit sms templates. Limit that rights to some main
application managers.

Provide some fiximps on sms dependent applications.

SPECIFICATIONS

Allow to send directly SMS when doing SMS marketing

SMS Application
  * in sms composer, in mass mode: rename Send SMS to Put in queue and
    add a Send Now button by-passing the queue;
  * set Put in queue as primary, Send Now as secondary;

SMS Marketing Application
  * in mailing view for SMS: rename Send SMS to Put in queue and
    add a Send Now button by-passing the queue;
  * set Put in queue as primary, Send Now as secondary;

Tests of SMS marketing with 400 contacts to SMS indicates it takes about
10 secondes to be completed with is considered as viable.

SMS Template access rights
GROUP-------------R-W-C-D-Note
Internal User-----X
​​​Admin / Settings--X-X-X-X
Stock Manager-----X-X-X-stock.picking
Sales Manager-----X-X-X-crm.lead, res.partner
Event Manager-----X-X-X-event.registration
Sub Manager-------X-X-X-sale.subscription, res.partner
MarkAut Manager---X-X-X-no limit
Account Manager---X-X-X-res.partner (followup)

Other groups
  * Online Appointment NO TEMPLATE USED
  * Accounting Manager NO TEMPLATE USED
  * SMS Marketing NO TEMPLATE USED
  * Studio Automated Action Need Technical Settings Anyway
  * Scheduled Actions Need Technical Settings Anyway
  * Server Action Need Technical Settings Anyway

LINKS

Task 2076366 (send now)
Task 2067873 (template access)
PR odoo/odoo#37298
PR odoo/enterprise#5750

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2019-09-25 18:45:15 +02:00
committed by GitHub
18 changed files with 111 additions and 36 deletions
+2
View File
@@ -10,6 +10,8 @@
'depends': ['crm', 'sms'],
'data': [
'views/crm_lead_views.xml',
'security/ir.model.access.csv',
'security/sms_security.xml',
],
'installable': True,
'application': False,
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sms_template_sale_manager,access.sms.template.sale.manager,sms.model_sms_template,sales_team.group_sale_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sms_template_sale_manager access.sms.template.sale.manager sms.model_sms_template sales_team.group_sale_manager 1 1 1 1
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_rule_sms_template_sale_manager" model="ir.rule">
<field name="name">SMS Template: sale manager CRUD on opportunity / partner templates</field>
<field name="model_id" ref="sms.model_sms_template"/>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]"/>
<field name="domain_force">[('res_model_id.model', 'in', ('crm.lead', 'res.partner'))]</field>
</record>
</odoo>
+2
View File
@@ -11,6 +11,8 @@
'data/sms_data.xml',
'views/event_views.xml',
'views/event_mail_views.xml',
'security/ir.model.access.csv',
'security/sms_security.xml',
],
'demo': [
],
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sms_template_event_manager,access.sms.template.event.manager,sms.model_sms_template,event.group_event_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sms_template_event_manager access.sms.template.event.manager sms.model_sms_template event.group_event_manager 1 1 1 1
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_rule_sms_template_event_manager" model="ir.rule">
<field name="name">SMS Template: event manager CRUD on event / registrations templates</field>
<field name="model_id" ref="sms.model_sms_template"/>
<field name="groups" eval="[(4, ref('event.group_event_manager'))]"/>
<field name="domain_force">[('res_model_id.model', 'in', ('event.event', 'event.registration'))]</field>
</record>
</odoo>
+4 -3
View File
@@ -1384,7 +1384,7 @@ class MailThread(models.AbstractModel):
msg_dict['cc'] = ','.join(email_cc_list) if email_cc_list else email_cc
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
# for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
msg_dict['recipients'] = ','.join(formatted_email
msg_dict['recipients'] = ','.join(set(formatted_email
for address in [
tools.decode_message_header(message, 'Delivered-To'),
tools.decode_message_header(message, 'To'),
@@ -1392,14 +1392,15 @@ class MailThread(models.AbstractModel):
tools.decode_message_header(message, 'Resent-To'),
tools.decode_message_header(message, 'Resent-Cc')
] if address
for formatted_email in tools.email_split_and_format(address)
for formatted_email in tools.email_split_and_format(address))
)
msg_dict['to'] = ','.join(formatted_email
msg_dict['to'] = ','.join(set(formatted_email
for address in [
tools.decode_message_header(message, 'Delivered-To'),
tools.decode_message_header(message, 'To')
] if address
for formatted_email in tools.email_split_and_format(address))
)
partner_ids = [x.id for x in self._mail_find_partner_from_emails(tools.email_split(msg_dict['recipients']), records=self) if x]
msg_dict['partner_ids'] = partner_ids
# compute references to find if email_message is a reply to an existing thread
+1 -1
View File
@@ -31,7 +31,7 @@ class MailingTrace(models.Model):
model = fields.Char(string='Document model')
res_id = fields.Integer(string='Document ID')
# campaign / wave data
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mailing', index=True)
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mailing', index=True, ondelete='cascade')
campaign_id = fields.Many2one(
related='mass_mailing_id.campaign_id',
string='Campaign',
@@ -20,6 +20,8 @@ class Mailing(models.Model):
sms_has_insufficient_credit = fields.Boolean(
'Insufficient IAP credits', compute='_compute_sms_has_insufficient_credit',
help='UX Field to propose to buy IAP credits')
sms_force_send = fields.Boolean(
'Send Directly', help='Use at your own risks.')
# opt_out_link
sms_allow_unsubscribe = fields.Boolean('Include opt-out link', default=True)
@@ -63,7 +65,30 @@ class Mailing(models.Model):
# --------------------------------------------------
def action_put_in_queue_sms(self):
return self.action_put_in_queue()
res = self.action_put_in_queue()
if self.sms_force_send:
self.action_send_mail()
return res
def action_send_now_sms(self):
if not self.sms_force_send:
self.write({'sms_force_send': True})
return self.action_send_mail()
def action_retry_failed(self):
mass_sms = self.filtered(lambda m: m.mailing_type == 'sms')
if mass_sms:
mass_sms.action_retry_failed_sms()
return super(Mailing, self - mass_sms).action_retry_failed()
def action_retry_failed_sms(self):
failed_sms = self.env['sms.sms'].sudo().search([
('mailing_id', 'in', self.ids),
('state', '=', 'error')
])
failed_sms.mapped('mailing_trace_ids').unlink()
failed_sms.unlink()
self.write({'state': 'in_queue'})
def action_test(self):
if self.mailing_type == 'sms':
@@ -166,6 +191,7 @@ class Mailing(models.Model):
'composition_mode': 'mass',
'mailing_id': self.id,
'mass_keep_log': self.keep_archives,
'mass_force_send': self.sms_force_send,
'mass_sms_allow_unsubscribe': self.sms_allow_unsubscribe,
}
@@ -183,11 +209,6 @@ class Mailing(models.Model):
raise UserError(_('There is no recipients selected.'))
composer = self.env['sms.composer'].with_context(active_id=False).create(mailing._send_sms_get_composer_values(res_ids))
# extra_context = self._get_mass_mailing_context()
# auto-commit except in testing mode
# auto_commit = not getattr(threading.currentThread(), 'testing', False)
# composer.send_mail(auto_commit=auto_commit)
composer._action_send_sms()
mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
return True
@@ -23,10 +23,14 @@
<attribute name="attrs">{'invisible': ['|', ('state', 'in', ('in_queue', 'done')), ('mailing_type', '!=', 'mail')]}</attribute>
</xpath>
<xpath expr="//button[@name='action_put_in_queue']" position="after">
<button name="action_put_in_queue" type="object"
class="oe_highlight" string="Send Now"
<button name="action_put_in_queue_sms" type="object"
class="oe_highlight" string="Put in Queue"
attrs="{'invisible': ['|', ('mailing_type', '=', 'mail'), ('state', 'in', ('in_queue', 'done'))]}"
confirm="This will send SMS to all recipients. Do you still want to proceed ?"/>
confirm="This will schedule an SMS marketing to all recipients. Do you still want to proceed ?"/>
<button name="action_send_now_sms" type="object"
string="Send Now"
attrs="{'invisible': ['|', ('mailing_type', '=', 'mail'), ('state', 'in', ('done'))]}"
confirm="This will send SMS to all recipients now. Do you still want to proceed ?"/>
</xpath>
<!-- Headers / Warnings -->
<xpath expr="//header" position="after">
@@ -90,8 +94,9 @@
</xpath>
<xpath expr="//page[@name='mail_body']" position="after">
<page string="SMS Content" name="sms_body" attrs="{'invisible': [('mailing_type', '!=', 'sms')]}">
<field name="body_plaintext" widget="sms_widget"/>
<field name="body_plaintext" widget="sms_widget" required="1"/>
<group>
<field name="sms_force_send" invisible="1"/>
<field name="sms_allow_unsubscribe"/>
</group>
</page>
@@ -74,12 +74,9 @@ class SMSComposer(models.TransientModel):
all_bodies[sms_id] = body
return all_bodies
def _prepare_mass_sms_values(self, records=None):
result = super(SMSComposer, self)._prepare_mass_sms_values(records=records)
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:
records = records if records is not None else self._get_records()
# for record_id, sms_values in result.items():
for record in records:
sms_values = result[record.id]
@@ -90,8 +87,8 @@ class SMSComposer(models.TransientModel):
})
return result
def _action_send_sms_mass(self, records=None):
sms_all = super(SMSComposer, self)._action_send_sms_mass()
def _prepare_mass_sms(self, records, sms_record_values):
sms_all = super(SMSComposer, self)._prepare_mass_sms(records, sms_record_values)
if self.mailing_id:
updated_bodies = sms_all._update_body_short_links()
for sms in sms_all:
+1 -1
View File
@@ -86,7 +86,7 @@ class SmsSms(models.Model):
return res
def _split_batch(self):
batch_size = int(self.env['ir.config_parameter'].sudo().get_param('sms.session.batch.size', 10))
batch_size = int(self.env['ir.config_parameter'].sudo().get_param('sms.session.batch.size', 500))
for sms_batch in tools.split_every(batch_size, self.ids):
yield sms_batch
+1 -1
View File
@@ -3,4 +3,4 @@ access_sms_sms_all,access.sms.sms.all,model_sms_sms,,0,0,0,0
access_sms_sms_system,access.sms.sms.system,model_sms_sms,base.group_system,1,1,1,1
access_sms_template_all,access.sms.template.all,model_sms_template,,0,0,0,0
access_sms_template_user,access.sms.template.user,model_sms_template,base.group_user,1,0,0,0
access_sms_template_system,access.sms.template.system,model_sms_template,base.group_system,1,1,1,0
access_sms_template_system,access.sms.template.system,model_sms_template,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_sms_sms_system access.sms.sms.system model_sms_sms base.group_system 1 1 1 1
4 access_sms_template_all access.sms.template.all model_sms_template 0 0 0 0
5 access_sms_template_user access.sms.template.user model_sms_template base.group_user 1 0 0 0
6 access_sms_template_system access.sms.template.system model_sms_template base.group_system 1 1 1 0 1
+19 -12
View File
@@ -147,7 +147,12 @@ class SendSMS(models.TransientModel):
self._action_send_sms()
return False
def _action_send_sms(self, force_send=False):
def action_send_sms_mass_now(self):
if not self.mass_force_send:
self.write({'mass_force_send': True})
return self.action_send_sms()
def _action_send_sms(self):
records = self._get_records()
if self.composition_mode == 'numbers':
return self._action_send_sms_numbers()
@@ -183,17 +188,16 @@ class SendSMS(models.TransientModel):
records = records if records is not None else self._get_records()
sms_record_values = self._prepare_mass_sms_values(records)
sms_create_vals = [sms_record_values[record.id] for record in records]
sms = self.env['sms.sms'].sudo().create(sms_create_vals)
sms_all = self._prepare_mass_sms(records, sms_record_values)
if sms and self.mass_keep_log and records and issubclass(type(records), self.pool['mail.thread']):
log_values = self._prepare_mass_log_values(sms_record_values)
if sms_all and self.mass_keep_log and records and issubclass(type(records), self.pool['mail.thread']):
log_values = self._prepare_mass_log_values(records, sms_record_values)
records._message_log_batch(**log_values)
if sms and self.mass_force_send:
sms.send(auto_commit=False, raise_exception=False)
return sms
if sms_all and self.mass_force_send:
sms_all.filtered(lambda sms: sms.state == 'outgoing').send(auto_commit=False, raise_exception=False)
return self.env['sms.sms'].sudo().search([('id', 'in', sms_all.ids)])
return sms_all
# ------------------------------------------------------------
# Mass mode specific
@@ -230,8 +234,7 @@ class SendSMS(models.TransientModel):
all_bodies = self.env['mail.template']._render_template(self.body, records._name, records.ids)
return all_bodies
def _prepare_mass_sms_values(self, records=None):
records = records if records is not None else self._get_records()
def _prepare_mass_sms_values(self, records):
all_bodies = self._prepare_body_values(records)
all_recipients = self._prepare_recipient_values(records)
blacklist_ids = self._get_blacklist_record_ids(records, all_recipients)
@@ -263,13 +266,17 @@ class SendSMS(models.TransientModel):
}
return result
def _prepare_mass_sms(self, records, sms_record_values):
sms_create_vals = [sms_record_values[record.id] for record in records]
return self.env['sms.sms'].sudo().create(sms_create_vals)
def _prepare_log_body_values(self, sms_records_values):
result = {}
for record_id, sms_values in sms_records_values.items():
result[record_id] = sms_values['body']
return result
def _prepare_mass_log_values(self, sms_records_values):
def _prepare_mass_log_values(self, records, sms_records_values):
return {
'bodies': self._prepare_log_body_values(sms_records_values),
'message_type': 'sms',
+6 -1
View File
@@ -39,7 +39,12 @@
</group>
</sheet>
<footer>
<button string="Send SMS" type="object" class="oe_highlight" name="action_send_sms"/>
<button string="Send SMS" type="object" class="oe_highlight" name="action_send_sms"
attrs="{'invisible': [('composition_mode', 'not in', ('comment', 'numbers'))]}"/>
<button string="Put in queue" type="object" class="oe_highlight" name="action_send_sms"
attrs="{'invisible': [('composition_mode', '!=', 'mass')]}"/>
<button string="Send Now" type="object" name="action_send_sms_mass_now"
attrs="{'invisible': [('composition_mode', '!=', 'mass')]}"/>
<button string="Close" class="btn btn-secondary" special="cancel"/>
</footer>
</form>
+2
View File
@@ -13,6 +13,8 @@
'data/mail_data.xml',
'views/res_config_settings_views.xml',
'wizard/confirm_stock_sms_views.xml',
'security/ir.model.access.csv',
'security/sms_security.xml',
],
'application': False,
'auto_install': True,
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sms_template_stock_manager,access.sms.template.stock.manager,sms.model_sms_template,stock.group_stock_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sms_template_stock_manager access.sms.template.stock.manager sms.model_sms_template stock.group_stock_manager 1 1 1 1
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_rule_sms_template_stock_manager" model="ir.rule">
<field name="name">SMS Template: stock manager CRUD on stock picking templates</field>
<field name="model_id" ref="sms.model_sms_template"/>
<field name="groups" eval="[(4, ref('stock.group_stock_manager'))]"/>
<field name="domain_force">[('res_model_id.model', '=', 'stock.picking')]</field>
</record>
</odoo>