mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-20 23:22:04 +02:00
Initial commit: Mail packages
This commit is contained in:
commit
4e53507711
1948 changed files with 751201 additions and 0 deletions
15
odoo-bringout-oca-ocb-sms/sms/models/__init__.py
Normal file
15
odoo-bringout-oca-ocb-sms/sms/models/__init__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_actions_server
|
||||
from . import ir_model
|
||||
from . import mail_followers
|
||||
from . import mail_message
|
||||
from . import mail_notification
|
||||
from . import mail_thread
|
||||
from . import mail_thread_phone
|
||||
from . import models
|
||||
from . import res_partner
|
||||
from . import sms_api
|
||||
from . import sms_sms
|
||||
from . import sms_template
|
||||
71
odoo-bringout-oca-ocb-sms/sms/models/ir_actions_server.py
Normal file
71
odoo-bringout-oca-ocb-sms/sms/models/ir_actions_server.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ServerActions(models.Model):
|
||||
""" Add SMS option in server actions. """
|
||||
_name = 'ir.actions.server'
|
||||
_inherit = ['ir.actions.server']
|
||||
|
||||
state = fields.Selection(selection_add=[
|
||||
('sms', 'Send SMS Text Message'),
|
||||
], ondelete={'sms': 'cascade'})
|
||||
# SMS
|
||||
sms_template_id = fields.Many2one(
|
||||
'sms.template', 'SMS Template',
|
||||
compute='_compute_sms_template_id',
|
||||
ondelete='set null', readonly=False, store=True,
|
||||
domain="[('model_id', '=', model_id)]",
|
||||
)
|
||||
sms_method = fields.Selection(
|
||||
selection=[('sms', 'SMS'), ('comment', 'Post as Message'), ('note', 'Post as Note')],
|
||||
string='Send as (SMS)',
|
||||
compute='_compute_sms_method',
|
||||
readonly=False, store=True,
|
||||
help='Choose method for SMS sending:\nSMS: mass SMS\nPost as Message: log on document\nPost as Note: mass SMS with archives')
|
||||
|
||||
@api.depends('model_id', 'state')
|
||||
def _compute_sms_template_id(self):
|
||||
to_reset = self.filtered(
|
||||
lambda act: act.state != 'sms' or \
|
||||
(act.model_id != act.sms_template_id.model_id)
|
||||
)
|
||||
if to_reset:
|
||||
to_reset.sms_template_id = False
|
||||
|
||||
@api.depends('state')
|
||||
def _compute_sms_method(self):
|
||||
to_reset = self.filtered(lambda act: act.state != 'sms')
|
||||
if to_reset:
|
||||
to_reset.sms_method = False
|
||||
other = self - to_reset
|
||||
if other:
|
||||
other.sms_method = 'sms'
|
||||
|
||||
def _check_model_coherency(self):
|
||||
super()._check_model_coherency()
|
||||
for action in self:
|
||||
if action.state == 'sms' and (action.model_id.transient or not action.model_id.is_mail_thread):
|
||||
raise ValidationError(_("Sending SMS can only be done on a not transient mail.thread model"))
|
||||
|
||||
def _run_action_sms_multi(self, eval_context=None):
|
||||
# TDE CLEANME: when going to new api with server action, remove action
|
||||
if not self.sms_template_id or self._is_recompute():
|
||||
return False
|
||||
|
||||
records = eval_context.get('records') or eval_context.get('record')
|
||||
if not records:
|
||||
return False
|
||||
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
default_res_model=records._name,
|
||||
default_res_ids=records.ids,
|
||||
default_composition_mode='comment' if self.sms_method == 'comment' else 'mass',
|
||||
default_template_id=self.sms_template_id.id,
|
||||
default_mass_keep_log=self.sms_method == 'note',
|
||||
).create({})
|
||||
composer.action_send_sms()
|
||||
return False
|
||||
41
odoo-bringout-oca-ocb-sms/sms/models/ir_model.py
Normal file
41
odoo-bringout-oca-ocb-sms/sms/models/ir_model.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class IrModel(models.Model):
|
||||
_inherit = 'ir.model'
|
||||
|
||||
is_mail_thread_sms = fields.Boolean(
|
||||
string="Mail Thread SMS", default=False,
|
||||
store=False, compute='_compute_is_mail_thread_sms', search='_search_is_mail_thread_sms',
|
||||
help="Whether this model supports messages and notifications through SMS",
|
||||
)
|
||||
|
||||
@api.depends('is_mail_thread')
|
||||
def _compute_is_mail_thread_sms(self):
|
||||
for model in self:
|
||||
if model.is_mail_thread:
|
||||
ModelObject = self.env[model.model]
|
||||
potential_fields = ModelObject._sms_get_number_fields() + ModelObject._sms_get_partner_fields()
|
||||
if any(fname in ModelObject._fields for fname in potential_fields):
|
||||
model.is_mail_thread_sms = True
|
||||
continue
|
||||
model.is_mail_thread_sms = False
|
||||
|
||||
def _search_is_mail_thread_sms(self, operator, value):
|
||||
thread_models = self.search([('is_mail_thread', '=', True)])
|
||||
valid_models = self.env['ir.model']
|
||||
for model in thread_models:
|
||||
if model.model not in self.env:
|
||||
continue
|
||||
ModelObject = self.env[model.model]
|
||||
potential_fields = ModelObject._sms_get_number_fields() + ModelObject._sms_get_partner_fields()
|
||||
if any(fname in ModelObject._fields for fname in potential_fields):
|
||||
valid_models |= model
|
||||
|
||||
search_sms = (operator == '=' and value) or (operator == '!=' and not value)
|
||||
if search_sms:
|
||||
return [('id', 'in', valid_models.ids)]
|
||||
return [('id', 'not in', valid_models.ids)]
|
||||
29
odoo-bringout-oca-ocb-sms/sms/models/mail_followers.py
Normal file
29
odoo-bringout-oca-ocb-sms/sms/models/mail_followers.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Followers(models.Model):
|
||||
_inherit = ['mail.followers']
|
||||
|
||||
def _get_recipient_data(self, records, message_type, subtype_id, pids=None):
|
||||
if message_type != 'sms' or not (pids or records):
|
||||
return super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids)
|
||||
|
||||
if pids is None and records:
|
||||
records_pids = dict(
|
||||
(record.id, record._sms_get_default_partners().ids)
|
||||
for record in records
|
||||
)
|
||||
elif pids and records:
|
||||
records_pids = dict((record.id, pids) for record in records)
|
||||
else:
|
||||
records_pids = {0: pids if pids else []}
|
||||
recipients_data = super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids)
|
||||
for rid, rdata in recipients_data.items():
|
||||
sms_pids = records_pids.get(rid) or []
|
||||
for pid, pdata in rdata.items():
|
||||
if pid in sms_pids:
|
||||
pdata['notif'] = 'sms'
|
||||
return recipients_data
|
||||
54
odoo-bringout-oca-ocb-sms/sms/models/mail_message.py
Normal file
54
odoo-bringout-oca-ocb-sms/sms/models/mail_message.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
|
||||
from odoo import exceptions, fields, models
|
||||
from odoo.tools import groupby
|
||||
|
||||
|
||||
class MailMessage(models.Model):
|
||||
""" Override MailMessage class in order to add a new type: SMS messages.
|
||||
Those messages comes with their own notification method, using SMS
|
||||
gateway. """
|
||||
_inherit = 'mail.message'
|
||||
|
||||
message_type = fields.Selection(selection_add=[
|
||||
('sms', 'SMS')
|
||||
], ondelete={'sms': lambda recs: recs.write({'message_type': 'email'})})
|
||||
has_sms_error = fields.Boolean(
|
||||
'Has SMS error', compute='_compute_has_sms_error', search='_search_has_sms_error')
|
||||
|
||||
def _compute_has_sms_error(self):
|
||||
sms_error_from_notification = self.env['mail.notification'].sudo().search([
|
||||
('notification_type', '=', 'sms'),
|
||||
('mail_message_id', 'in', self.ids),
|
||||
('notification_status', '=', 'exception')]).mapped('mail_message_id')
|
||||
for message in self:
|
||||
message.has_sms_error = message in sms_error_from_notification
|
||||
|
||||
def _search_has_sms_error(self, operator, operand):
|
||||
if operator == '=' and operand:
|
||||
return ['&', ('notification_ids.notification_status', '=', 'exception'), ('notification_ids.notification_type', '=', 'sms')]
|
||||
raise NotImplementedError()
|
||||
|
||||
def message_format(self, format_reply=True):
|
||||
""" Override in order to retrieves data about SMS (recipient name and
|
||||
SMS status)
|
||||
|
||||
TDE FIXME: clean the overall message_format thingy
|
||||
"""
|
||||
message_values = super(MailMessage, self).message_format(format_reply=format_reply)
|
||||
all_sms_notifications = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', 'in', [r['id'] for r in message_values]),
|
||||
('notification_type', '=', 'sms')
|
||||
])
|
||||
msgid_to_notif = defaultdict(lambda: self.env['mail.notification'].sudo())
|
||||
for notif in all_sms_notifications:
|
||||
msgid_to_notif[notif.mail_message_id.id] += notif
|
||||
|
||||
for message in message_values:
|
||||
customer_sms_data = [(notif.id, notif.res_partner_id.display_name or notif.sms_number, notif.notification_status) for notif in msgid_to_notif.get(message['id'], [])]
|
||||
message['sms_ids'] = customer_sms_data
|
||||
return message_values
|
||||
21
odoo-bringout-oca-ocb-sms/sms/models/mail_notification.py
Normal file
21
odoo-bringout-oca-ocb-sms/sms/models/mail_notification.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MailNotification(models.Model):
|
||||
_inherit = 'mail.notification'
|
||||
|
||||
notification_type = fields.Selection(selection_add=[
|
||||
('sms', 'SMS')
|
||||
], ondelete={'sms': 'cascade'})
|
||||
sms_id = fields.Many2one('sms.sms', string='SMS', index='btree_not_null', ondelete='set null')
|
||||
sms_number = fields.Char('SMS Number', groups='base.group_user')
|
||||
failure_type = fields.Selection(selection_add=[
|
||||
('sms_number_missing', 'Missing Number'),
|
||||
('sms_number_format', 'Wrong Number Format'),
|
||||
('sms_credit', 'Insufficient Credit'),
|
||||
('sms_server', 'Server Error'),
|
||||
('sms_acc', 'Unregistered Account')
|
||||
])
|
||||
276
odoo-bringout-oca-ocb-sms/sms/models/mail_thread.py
Normal file
276
odoo-bringout-oca-ocb-sms/sms/models/mail_thread.py
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, models, fields
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
from odoo.addons.sms.tools.sms_tools import sms_content_to_rendered_html
|
||||
from odoo.tools import html2plaintext
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
message_has_sms_error = fields.Boolean(
|
||||
'SMS Delivery error', compute='_compute_message_has_sms_error', search='_search_message_has_sms_error',
|
||||
help="If checked, some messages have a delivery error.")
|
||||
|
||||
def _compute_message_has_sms_error(self):
|
||||
res = {}
|
||||
if self.ids:
|
||||
self.env.cr.execute("""
|
||||
SELECT msg.res_id, COUNT(msg.res_id)
|
||||
FROM mail_message msg
|
||||
INNER JOIN mail_notification notif
|
||||
ON notif.mail_message_id = msg.id
|
||||
WHERE notif.notification_type = 'sms'
|
||||
AND notif.notification_status = 'exception'
|
||||
AND notif.author_id = %(author_id)s
|
||||
AND msg.model = %(model_name)s
|
||||
AND msg.res_id in %(res_ids)s
|
||||
AND msg.message_type != 'user_notification'
|
||||
GROUP BY msg.res_id
|
||||
""", {'author_id': self.env.user.partner_id.id, 'model_name': self._name, 'res_ids': tuple(self.ids)})
|
||||
res.update(self._cr.fetchall())
|
||||
|
||||
for record in self:
|
||||
record.message_has_sms_error = bool(res.get(record._origin.id, 0))
|
||||
|
||||
@api.model
|
||||
def _search_message_has_sms_error(self, operator, operand):
|
||||
return ['&', ('message_ids.has_sms_error', operator, operand), ('message_ids.author_id', '=', self.env.user.partner_id.id)]
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, *args, body='', message_type='notification', **kwargs):
|
||||
# When posting an 'SMS' `message_type`, make sure that the body is used as-is in the sms,
|
||||
# and reformat the message body for the notification (mainly making URLs clickable).
|
||||
if message_type == 'sms':
|
||||
kwargs['sms_content'] = body
|
||||
body = sms_content_to_rendered_html(body)
|
||||
return super().message_post(*args, body=body, message_type=message_type, **kwargs)
|
||||
|
||||
def _message_sms_schedule_mass(self, body='', template=False, **composer_values):
|
||||
""" Shortcut method to schedule a mass sms sending on a recordset.
|
||||
|
||||
:param template: an optional sms.template record;
|
||||
"""
|
||||
composer_context = {
|
||||
'default_res_model': self._name,
|
||||
'default_composition_mode': 'mass',
|
||||
'default_template_id': template.id if template else False,
|
||||
'default_res_ids': self.ids,
|
||||
}
|
||||
if body and not template:
|
||||
composer_context['default_body'] = body
|
||||
|
||||
create_vals = {
|
||||
'mass_force_send': False,
|
||||
'mass_keep_log': True,
|
||||
}
|
||||
if composer_values:
|
||||
create_vals.update(composer_values)
|
||||
|
||||
composer = self.env['sms.composer'].with_context(**composer_context).create(create_vals)
|
||||
return composer._action_send_sms()
|
||||
|
||||
def _message_sms_with_template(self, template=False, template_xmlid=False, template_fallback='', partner_ids=False, **kwargs):
|
||||
""" Shortcut method to perform a _message_sms with an sms.template.
|
||||
|
||||
:param template: a valid sms.template record;
|
||||
:param template_xmlid: XML ID of an sms.template (if no template given);
|
||||
:param template_fallback: plaintext (inline_template-enabled) in case template
|
||||
and template xml id are falsy (for example due to deleted data);
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not template and template_xmlid:
|
||||
template = self.env.ref(template_xmlid, raise_if_not_found=False)
|
||||
if template:
|
||||
body = template._render_field('body', self.ids, compute_lang=True)[self.id]
|
||||
else:
|
||||
body = self.env['sms.template']._render_template(template_fallback, self._name, self.ids)[self.id]
|
||||
return self._message_sms(body, partner_ids=partner_ids, **kwargs)
|
||||
|
||||
def _message_sms(self, body, subtype_id=False, partner_ids=False, number_field=False,
|
||||
sms_numbers=None, sms_pid_to_number=None, **kwargs):
|
||||
""" Main method to post a message on a record using SMS-based notification
|
||||
method.
|
||||
|
||||
:param body: content of SMS;
|
||||
:param subtype_id: mail.message.subtype used in mail.message associated
|
||||
to the sms notification process;
|
||||
:param partner_ids: if set is a record set of partners to notify;
|
||||
:param number_field: if set is a name of field to use on current record
|
||||
to compute a number to notify;
|
||||
:param sms_numbers: see ``_notify_thread_by_sms``;
|
||||
:param sms_pid_to_number: see ``_notify_thread_by_sms``;
|
||||
"""
|
||||
self.ensure_one()
|
||||
sms_pid_to_number = sms_pid_to_number if sms_pid_to_number is not None else {}
|
||||
|
||||
if number_field or (partner_ids is False and sms_numbers is None):
|
||||
info = self._sms_get_recipients_info(force_field=number_field)[self.id]
|
||||
info_partner_ids = info['partner'].ids if info['partner'] else False
|
||||
info_number = info['sanitized'] if info['sanitized'] else info['number']
|
||||
if info_partner_ids and info_number:
|
||||
sms_pid_to_number[info_partner_ids[0]] = info_number
|
||||
if info_partner_ids:
|
||||
partner_ids = info_partner_ids + (partner_ids or [])
|
||||
if not info_partner_ids:
|
||||
if info_number:
|
||||
sms_numbers = [info_number] + (sms_numbers or [])
|
||||
# will send a falsy notification allowing to fix it through SMS wizards
|
||||
elif not sms_numbers:
|
||||
sms_numbers = [False]
|
||||
|
||||
if subtype_id is False:
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
|
||||
return self.message_post(
|
||||
body=body, partner_ids=partner_ids or [], # TDE FIXME: temp fix otherwise crash mail_thread.py
|
||||
message_type='sms', subtype_id=subtype_id,
|
||||
sms_numbers=sms_numbers, sms_pid_to_number=sms_pid_to_number,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def _notify_thread(self, message, msg_vals=False, **kwargs):
|
||||
recipients_data = super(MailThread, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
|
||||
self._notify_thread_by_sms(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
return recipients_data
|
||||
|
||||
def _notify_thread_by_sms(self, message, recipients_data, msg_vals=False,
|
||||
sms_content=None, sms_numbers=None, sms_pid_to_number=None,
|
||||
resend_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_content: plaintext version of body, mainly to avoid
|
||||
conversion glitches by splitting html and plain text content formatting
|
||||
(e.g.: links, styling.).
|
||||
If not given, `msg_vals`'s `body` is used and converted from html to plaintext;
|
||||
: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 resend_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 = sms_content or html2plaintext(msg_vals['body'] if msg_vals and 'body' in msg_vals else message.body)
|
||||
sms_base_vals = {
|
||||
'body': 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()
|
||||
]
|
||||
existing_partners_numbers = {vals_dict['number'] for vals_dict in sms_create_vals}
|
||||
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 if n not in existing_partners_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 resend_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
|
||||
|
||||
@api.model
|
||||
def notify_cancel_by_type(self, notification_type):
|
||||
super().notify_cancel_by_type(notification_type)
|
||||
if notification_type == 'sms':
|
||||
# TDE CHECK: delete pending SMS
|
||||
self._notify_cancel_by_type_generic('sms')
|
||||
return True
|
||||
16
odoo-bringout-oca-ocb-sms/sms/models/mail_thread_phone.py
Normal file
16
odoo-bringout-oca-ocb-sms/sms/models/mail_thread_phone.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class PhoneMixin(models.AbstractModel):
|
||||
_inherit = 'mail.thread.phone'
|
||||
|
||||
def _sms_get_number_fields(self):
|
||||
""" Add fields coming from mail.thread.phone implementation. """
|
||||
phone_fields = self._phone_get_number_fields()
|
||||
sms_fields = super(PhoneMixin, self)._sms_get_number_fields()
|
||||
for fname in (f for f in sms_fields if f not in phone_fields):
|
||||
phone_fields.append(fname)
|
||||
return phone_fields
|
||||
117
odoo-bringout-oca-ocb-sms/sms/models/models.py
Normal file
117
odoo-bringout-oca-ocb-sms/sms/models/models.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
from odoo import models
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
|
||||
|
||||
class BaseModel(models.AbstractModel):
|
||||
_inherit = 'base'
|
||||
|
||||
def _sms_get_partner_fields(self):
|
||||
""" This method returns the fields to use to find the contact to link
|
||||
whensending an SMS. Having partner is not necessary, having only phone
|
||||
number fields is possible. However it gives more flexibility to
|
||||
notifications management when having partners. """
|
||||
fields = []
|
||||
if hasattr(self, 'partner_id'):
|
||||
fields.append('partner_id')
|
||||
if hasattr(self, 'partner_ids'):
|
||||
fields.append('partner_ids')
|
||||
return fields
|
||||
|
||||
def _sms_get_default_partners(self):
|
||||
""" This method will likely need to be overridden by inherited models.
|
||||
:returns partners: recordset of res.partner
|
||||
"""
|
||||
partners = self.env['res.partner']
|
||||
for fname in self._sms_get_partner_fields():
|
||||
partners = partners.union(*self.mapped(fname)) # ensure ordering
|
||||
return partners
|
||||
|
||||
def _sms_get_number_fields(self):
|
||||
""" This method returns the fields to use to find the number to use to
|
||||
send an SMS on a record. """
|
||||
if 'mobile' in self:
|
||||
return ['mobile']
|
||||
return []
|
||||
|
||||
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
|
||||
20
odoo-bringout-oca-ocb-sms/sms/models/res_partner.py
Normal file
20
odoo-bringout-oca-ocb-sms/sms/models/res_partner.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_name = 'res.partner'
|
||||
_inherit = ['mail.thread.phone', 'res.partner']
|
||||
|
||||
def _sms_get_default_partners(self):
|
||||
""" Override of mail.thread method.
|
||||
SMS recipients on partners are the partners themselves.
|
||||
"""
|
||||
return self
|
||||
|
||||
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 ['mobile', 'phone']
|
||||
79
odoo-bringout-oca-ocb-sms/sms/models/sms_api.py
Normal file
79
odoo-bringout-oca-ocb-sms/sms/models/sms_api.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, exceptions, models
|
||||
from odoo.addons.iap.tools import iap_tools
|
||||
|
||||
DEFAULT_ENDPOINT = 'https://iap-sms.odoo.com'
|
||||
|
||||
|
||||
class SmsApi(models.AbstractModel):
|
||||
_name = 'sms.api'
|
||||
_description = 'SMS API'
|
||||
|
||||
@api.model
|
||||
def _contact_iap(self, local_endpoint, params):
|
||||
if not self.env.registry.ready: # Don't reach IAP servers during module installation
|
||||
raise exceptions.AccessError("Unavailable during module installation.")
|
||||
|
||||
account = self.env['iap.account'].get('sms')
|
||||
params['account_token'] = account.account_token
|
||||
endpoint = self.env['ir.config_parameter'].sudo().get_param('sms.endpoint', DEFAULT_ENDPOINT)
|
||||
# TODO PRO, the default timeout is 15, do we have to increase it ?
|
||||
return iap_tools.iap_jsonrpc(endpoint + local_endpoint, params=params)
|
||||
|
||||
@api.model
|
||||
def _send_sms(self, numbers, message):
|
||||
""" Send a single message to several numbers
|
||||
|
||||
:param numbers: list of E164 formatted phone numbers
|
||||
:param message: content to send
|
||||
|
||||
:raises ? TDE FIXME
|
||||
"""
|
||||
params = {
|
||||
'numbers': numbers,
|
||||
'message': message,
|
||||
}
|
||||
return self._contact_iap('/iap/message_send', params)
|
||||
|
||||
@api.model
|
||||
def _send_sms_batch(self, messages):
|
||||
""" Send SMS using IAP in batch mode
|
||||
|
||||
:param messages: list of SMS to send, structured as dict [{
|
||||
'res_id': integer: ID of sms.sms,
|
||||
'number': string: E164 formatted phone number,
|
||||
'content': string: content to send
|
||||
}]
|
||||
|
||||
:return: return of /iap/sms/1/send controller which is a list of dict [{
|
||||
'res_id': integer: ID of sms.sms,
|
||||
'state': string: 'insufficient_credit' or 'wrong_number_format' or 'success',
|
||||
'credit': integer: number of credits spent to send this SMS,
|
||||
}]
|
||||
|
||||
:raises: normally none
|
||||
"""
|
||||
params = {
|
||||
'messages': messages
|
||||
}
|
||||
return self._contact_iap('/iap/sms/2/send', params)
|
||||
|
||||
@api.model
|
||||
def _get_sms_api_error_messages(self):
|
||||
""" Returns a dict containing the error message to display for every known error 'state'
|
||||
resulting from the '_send_sms_batch' method.
|
||||
We prefer a dict instead of a message-per-error-state based method so we only call
|
||||
the 'get_credits_url' once, to avoid extra RPC calls. """
|
||||
|
||||
buy_credits_url = self.sudo().env['iap.account'].get_credits_url(service_name='sms')
|
||||
buy_credits = '<a href="%s" target="_blank">%s</a>' % (
|
||||
buy_credits_url,
|
||||
_('Buy credits.')
|
||||
)
|
||||
return {
|
||||
'unregistered': _("You don't have an eligible IAP account."),
|
||||
'insufficient_credit': ' '.join([_('You don\'t have enough credits on your IAP account.'), buy_credits]),
|
||||
'wrong_number_format': _("The number you're trying to reach is not correctly formatted."),
|
||||
}
|
||||
217
odoo-bringout-oca-ocb-sms/sms/models/sms_sms.py
Normal file
217
odoo-bringout-oca-ocb-sms/sms/models/sms_sms.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SmsSms(models.Model):
|
||||
_name = 'sms.sms'
|
||||
_description = 'Outgoing SMS'
|
||||
_rec_name = 'number'
|
||||
_order = 'id DESC'
|
||||
|
||||
IAP_TO_SMS_STATE = {
|
||||
'success': 'sent',
|
||||
'insufficient_credit': 'sms_credit',
|
||||
'wrong_number_format': 'sms_number_format',
|
||||
'server_error': 'sms_server',
|
||||
'unregistered': 'sms_acc'
|
||||
}
|
||||
|
||||
number = fields.Char('Number')
|
||||
body = fields.Text()
|
||||
partner_id = fields.Many2one('res.partner', 'Customer')
|
||||
mail_message_id = fields.Many2one('mail.message', index=True)
|
||||
state = fields.Selection([
|
||||
('outgoing', 'In Queue'),
|
||||
('sent', 'Sent'),
|
||||
('error', 'Error'),
|
||||
('canceled', 'Canceled')
|
||||
], 'SMS Status', readonly=True, copy=False, default='outgoing', required=True)
|
||||
failure_type = fields.Selection([
|
||||
('sms_number_missing', 'Missing Number'),
|
||||
('sms_number_format', 'Wrong Number Format'),
|
||||
('sms_credit', 'Insufficient Credit'),
|
||||
('sms_server', 'Server Error'),
|
||||
('sms_acc', 'Unregistered Account'),
|
||||
# mass mode specific codes
|
||||
('sms_blacklist', 'Blacklisted'),
|
||||
('sms_duplicate', 'Duplicate'),
|
||||
('sms_optout', 'Opted Out'),
|
||||
], copy=False)
|
||||
|
||||
def action_set_canceled(self):
|
||||
self.state = 'canceled'
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('sms_id', 'in', self.ids),
|
||||
# sent is sent -> cannot reset
|
||||
('notification_status', 'not in', ['canceled', 'sent']),
|
||||
])
|
||||
if notifications:
|
||||
notifications.write({'notification_status': 'canceled'})
|
||||
if not self._context.get('sms_skip_msg_notification', False):
|
||||
notifications.mail_message_id._notify_message_notification_update()
|
||||
|
||||
def action_set_error(self, failure_type):
|
||||
self.state = 'error'
|
||||
self.failure_type = failure_type
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('sms_id', 'in', self.ids),
|
||||
# sent can be set to error due to IAP feedback
|
||||
('notification_status', '!=', 'exception'),
|
||||
])
|
||||
if notifications:
|
||||
notifications.write({'notification_status': 'exception', 'failure_type': failure_type})
|
||||
if not self._context.get('sms_skip_msg_notification', False):
|
||||
notifications.mail_message_id._notify_message_notification_update()
|
||||
|
||||
def action_set_outgoing(self):
|
||||
self.write({
|
||||
'state': 'outgoing',
|
||||
'failure_type': False
|
||||
})
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('sms_id', 'in', self.ids),
|
||||
# sent is sent -> cannot reset
|
||||
('notification_status', 'not in', ['ready', 'sent']),
|
||||
])
|
||||
if notifications:
|
||||
notifications.write({'notification_status': 'ready', 'failure_type': False})
|
||||
if not self._context.get('sms_skip_msg_notification', False):
|
||||
notifications.mail_message_id._notify_message_notification_update()
|
||||
|
||||
def send(self, unlink_failed=False, unlink_sent=True, auto_commit=False, raise_exception=False):
|
||||
""" Main API method to send SMS.
|
||||
|
||||
:param unlink_failed: unlink failed SMS after IAP feedback;
|
||||
:param unlink_sent: unlink sent SMS after IAP feedback;
|
||||
:param auto_commit: commit after each batch of SMS;
|
||||
:param raise_exception: raise if there is an issue contacting IAP;
|
||||
"""
|
||||
self = self.filtered(lambda sms: sms.state == 'outgoing')
|
||||
for batch_ids in self._split_batch():
|
||||
self.browse(batch_ids)._send(unlink_failed=unlink_failed, unlink_sent=unlink_sent, raise_exception=raise_exception)
|
||||
# auto-commit if asked except in testing mode
|
||||
if auto_commit is True and not getattr(threading.current_thread(), 'testing', False):
|
||||
self._cr.commit()
|
||||
|
||||
def resend_failed(self):
|
||||
sms_to_send = self.filtered(lambda sms: sms.state == 'error')
|
||||
sms_to_send.state = 'outgoing'
|
||||
notification_title = _('Warning')
|
||||
notification_type = 'danger'
|
||||
|
||||
if sms_to_send:
|
||||
sms_to_send.send()
|
||||
success_sms = len(sms_to_send) - len(sms_to_send.exists())
|
||||
if success_sms > 0:
|
||||
notification_title = _('Success')
|
||||
notification_type = 'success'
|
||||
notification_message = _('%s out of the %s selected SMS Text Messages have successfully been resent.', success_sms, len(self))
|
||||
else:
|
||||
notification_message = _('The SMS Text Messages could not be resent.')
|
||||
else:
|
||||
notification_message = _('There are no SMS Text Messages to resend.')
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': notification_title,
|
||||
'message': notification_message,
|
||||
'type': notification_type,
|
||||
}
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _process_queue(self, ids=None):
|
||||
""" Send immediately queued messages, committing after each message is sent.
|
||||
This is not transactional and should not be called during another transaction!
|
||||
|
||||
:param list ids: optional list of emails ids to send. If passed no search
|
||||
is performed, and these ids are used instead.
|
||||
"""
|
||||
domain = [('state', '=', 'outgoing')]
|
||||
|
||||
filtered_ids = self.search(domain, limit=10000).ids # TDE note: arbitrary limit we might have to update
|
||||
if ids:
|
||||
ids = list(set(filtered_ids) & set(ids))
|
||||
else:
|
||||
ids = filtered_ids
|
||||
ids.sort()
|
||||
|
||||
res = None
|
||||
try:
|
||||
# auto-commit except in testing mode
|
||||
auto_commit = not getattr(threading.current_thread(), 'testing', False)
|
||||
res = self.browse(ids).send(unlink_failed=False, unlink_sent=True, auto_commit=auto_commit, raise_exception=False)
|
||||
except Exception:
|
||||
_logger.exception("Failed processing SMS queue")
|
||||
return res
|
||||
|
||||
def _split_batch(self):
|
||||
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
|
||||
|
||||
def _send(self, unlink_failed=False, unlink_sent=True, raise_exception=False):
|
||||
""" This method tries to send SMS after checking the number (presence and
|
||||
formatting). """
|
||||
iap_data = [{
|
||||
'res_id': record.id,
|
||||
'number': record.number,
|
||||
'content': record.body,
|
||||
} for record in self]
|
||||
|
||||
try:
|
||||
iap_results = self.env['sms.api']._send_sms_batch(iap_data)
|
||||
except Exception as e:
|
||||
_logger.info('Sent batch %s SMS: %s: failed with exception %s', len(self.ids), self.ids, e)
|
||||
if raise_exception:
|
||||
raise
|
||||
self._postprocess_iap_sent_sms(
|
||||
[{'res_id': sms.id, 'state': 'server_error'} for sms in self],
|
||||
unlink_failed=unlink_failed, unlink_sent=unlink_sent)
|
||||
else:
|
||||
_logger.info('Send batch %s SMS: %s: gave %s', len(self.ids), self.ids, iap_results)
|
||||
self._postprocess_iap_sent_sms(iap_results, unlink_failed=unlink_failed, unlink_sent=unlink_sent)
|
||||
|
||||
def _postprocess_iap_sent_sms(self, iap_results, failure_reason=None, unlink_failed=False, unlink_sent=True):
|
||||
todelete_sms_ids = []
|
||||
if unlink_failed:
|
||||
todelete_sms_ids += [item['res_id'] for item in iap_results if item['state'] != 'success']
|
||||
if unlink_sent:
|
||||
todelete_sms_ids += [item['res_id'] for item in iap_results if item['state'] == 'success']
|
||||
|
||||
for state in self.IAP_TO_SMS_STATE.keys():
|
||||
sms_ids = [item['res_id'] for item in iap_results if item['state'] == state]
|
||||
if sms_ids:
|
||||
if state != 'success' and not unlink_failed:
|
||||
self.env['sms.sms'].sudo().browse(sms_ids).write({
|
||||
'state': 'error',
|
||||
'failure_type': self.IAP_TO_SMS_STATE[state],
|
||||
})
|
||||
if state == 'success' and not unlink_sent:
|
||||
self.env['sms.sms'].sudo().browse(sms_ids).write({
|
||||
'state': 'sent',
|
||||
'failure_type': False,
|
||||
})
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('notification_type', '=', 'sms'),
|
||||
('sms_id', 'in', sms_ids),
|
||||
('notification_status', 'not in', ('sent', 'canceled')),
|
||||
])
|
||||
if notifications:
|
||||
notifications.write({
|
||||
'notification_status': 'sent' if state == 'success' else 'exception',
|
||||
'failure_type': self.IAP_TO_SMS_STATE[state] if state != 'success' else False,
|
||||
'failure_reason': failure_reason if failure_reason else False,
|
||||
})
|
||||
self.mail_message_id._notify_message_notification_update()
|
||||
|
||||
if todelete_sms_ids:
|
||||
self.browse(todelete_sms_ids).sudo().unlink()
|
||||
78
odoo-bringout-oca-ocb-sms/sms/models/sms_template.py
Normal file
78
odoo-bringout-oca-ocb-sms/sms/models/sms_template.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class SMSTemplate(models.Model):
|
||||
"Templates for sending SMS"
|
||||
_name = "sms.template"
|
||||
_inherit = ['mail.render.mixin', 'template.reset.mixin']
|
||||
_description = 'SMS Templates'
|
||||
|
||||
_unrestricted_rendering = True
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(SMSTemplate, self).default_get(fields)
|
||||
if not fields or 'model_id' in fields and not res.get('model_id') and res.get('model'):
|
||||
res['model_id'] = self.env['ir.model']._get(res['model']).id
|
||||
return res
|
||||
|
||||
name = fields.Char('Name', translate=True)
|
||||
model_id = fields.Many2one(
|
||||
'ir.model', string='Applies to', required=True,
|
||||
domain=['&', ('is_mail_thread_sms', '=', True), ('transient', '=', False)],
|
||||
help="The type of document this template can be used with", ondelete='cascade')
|
||||
model = fields.Char('Related Document Model', related='model_id.model', index=True, store=True, readonly=True)
|
||||
body = fields.Char('Body', translate=True, required=True)
|
||||
# Use to create contextual action (same as for email template)
|
||||
sidebar_action_id = fields.Many2one('ir.actions.act_window', 'Sidebar action', readonly=True, copy=False,
|
||||
help="Sidebar action to make this template available on records "
|
||||
"of the related document model")
|
||||
|
||||
# Overrides of mail.render.mixin
|
||||
@api.depends('model')
|
||||
def _compute_render_model(self):
|
||||
for template in self:
|
||||
template.render_model = template.model
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# CRUD
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
default = dict(default or {},
|
||||
name=_("%s (copy)", self.name))
|
||||
return super(SMSTemplate, self).copy(default=default)
|
||||
|
||||
def unlink(self):
|
||||
self.sudo().mapped('sidebar_action_id').unlink()
|
||||
return super(SMSTemplate, self).unlink()
|
||||
|
||||
def action_create_sidebar_action(self):
|
||||
ActWindow = self.env['ir.actions.act_window']
|
||||
view = self.env.ref('sms.sms_composer_view_form')
|
||||
|
||||
for template in self:
|
||||
button_name = _('Send SMS (%s)', template.name)
|
||||
action = ActWindow.create({
|
||||
'name': button_name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'sms.composer',
|
||||
# Add default_composition_mode to guess to determine if need to use mass or comment composer
|
||||
'context': "{'default_template_id' : %d, 'sms_composition_mode': 'guess', 'default_res_ids': active_ids, 'default_res_id': active_id}" % (template.id),
|
||||
'view_mode': 'form',
|
||||
'view_id': view.id,
|
||||
'target': 'new',
|
||||
'binding_model_id': template.model_id.id,
|
||||
})
|
||||
template.write({'sidebar_action_id': action.id})
|
||||
return True
|
||||
|
||||
def action_unlink_sidebar_action(self):
|
||||
for template in self:
|
||||
if template.sidebar_action_id:
|
||||
template.sidebar_action_id.unlink()
|
||||
return True
|
||||
Loading…
Add table
Add a link
Reference in a new issue