[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:
@@ -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
|
||||
|
@@ -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>
|
||||
@@ -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
|
||||
|
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
@@ -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>
|
||||
Reference in New Issue
Block a user