diff --git a/addons/calendar_sms/__init__.py b/addons/calendar_sms/__init__.py
new file mode 100644
index 00000000000..dc5e6b693d1
--- /dev/null
+++ b/addons/calendar_sms/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
diff --git a/addons/calendar_sms/__manifest__.py b/addons/calendar_sms/__manifest__.py
new file mode 100644
index 00000000000..0e734074aaa
--- /dev/null
+++ b/addons/calendar_sms/__manifest__.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': "Calendar SMS",
+ 'summary': 'Send text messages as event reminders',
+ 'description': "Send text messages as event reminders",
+ 'category': 'Hidden',
+ 'version': '1.0',
+ 'depends': ['calendar', 'sms'],
+ 'data': [
+ 'views/calendar_views.xml',
+ ],
+ 'application': False,
+ 'auto_install': True,
+}
diff --git a/addons/calendar_sms/i18n/calendar_sms.pot b/addons/calendar_sms/i18n/calendar_sms.pot
new file mode 100644
index 00000000000..073430308ff
--- /dev/null
+++ b/addons/calendar_sms/i18n/calendar_sms.pot
@@ -0,0 +1,49 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * calendar_sms
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 10.saas~18+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-09-20 11:33+0000\n"
+"PO-Revision-Date: 2017-09-20 11:33+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: calendar_sms
+#: model:ir.model,name:calendar_sms.model_calendar_event
+msgid "Event"
+msgstr ""
+
+#. module: calendar_sms
+#: model:ir.model,name:calendar_sms.model_calendar_alarm
+msgid "Event alarm"
+msgstr ""
+
+#. module: calendar_sms
+#: code:addons/calendar_sms/models/calendar.py:23
+#, python-format
+msgid "Event reminder: %s on %s."
+msgstr ""
+
+#. module: calendar_sms
+#: code:addons/calendar_sms/models/calendar.py:24
+#, python-format
+msgid "SMS text message reminder sent !"
+msgstr ""
+
+#. module: calendar_sms
+#: model:ir.actions.act_window,name:calendar_sms.sms_message_send_action_mutli
+msgid "Send SMS to attendees"
+msgstr ""
+
+#. module: calendar_sms
+#: model:ir.model,name:calendar_sms.model_calendar_alarm_manager
+msgid "calendar.alarm_manager"
+msgstr ""
+
diff --git a/addons/calendar_sms/models/__init__.py b/addons/calendar_sms/models/__init__.py
new file mode 100644
index 00000000000..a71750ab34d
--- /dev/null
+++ b/addons/calendar_sms/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import calendar
diff --git a/addons/calendar_sms/models/calendar.py b/addons/calendar_sms/models/calendar.py
new file mode 100644
index 00000000000..d22200b9363
--- /dev/null
+++ b/addons/calendar_sms/models/calendar.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+
+from odoo import api, fields, models, _
+
+_logger = logging.getLogger(__name__)
+
+
+class CalendarEvent(models.Model):
+ _inherit = 'calendar.event'
+
+ def _get_default_sms_recipients(self):
+ """ Method overriden from mail.thread (defined in the sms module).
+ SMS text messages will be sent to attendees that haven't declined the event(s).
+ """
+ return self.mapped('attendee_ids').filtered(lambda att: att.state != 'declined').mapped('partner_id')
+
+ def _do_sms_reminder(self):
+ """ Send an SMS text reminder to attendees that haven't declined the event """
+ for event in self:
+ sms_msg = _("Event reminder: %s on %s.") % (event.name, event.start_datetime or event.start_date)
+ note_msg = _('SMS text message reminder sent !')
+ event.message_post_send_sms(sms_msg, note_msg=note_msg)
+
+
+class CalendarAlarm(models.Model):
+ _inherit = 'calendar.alarm'
+
+ type = fields.Selection(selection_add=[('sms', 'SMS Text Message')])
+
+
+class AlarmManager(models.AbstractModel):
+ _inherit = 'calendar.alarm_manager'
+
+ @api.model
+ def get_next_mail(self):
+ """ Cron method, overriden here to send SMS reminders as well
+ """
+ result = super(AlarmManager, self).get_next_mail()
+ now = fields.Datetime.now()
+ last_sms_cron = self.env['ir.config_parameter'].get_param('calendar_sms.last_sms_cron', default=now)
+ cron = self.env['ir.model.data'].get_object('calendar', 'ir_cron_scheduler_alarm')
+
+ interval_to_second = {
+ "weeks": 7 * 24 * 60 * 60,
+ "days": 24 * 60 * 60,
+ "hours": 60 * 60,
+ "minutes": 60,
+ "seconds": 1
+ }
+
+ cron_interval = cron.interval_number * interval_to_second[cron.interval_type]
+ events_data = self.get_next_potential_limit_alarm('sms', seconds=cron_interval)
+
+ for event in self.env['calendar.event'].browse(events_data):
+ max_delta = events_data[event.id]['max_duration']
+
+ if event.recurrency:
+ found = False
+ for event_start in event._get_recurrent_date_by_event():
+ event_start = event_start.replace(tzinfo=None)
+ last_found = self.do_check_alarm_for_one_date(event_start, event, max_delta, 0, 'sms', after=last_sms_cron, missing=True)
+ for alert in last_found:
+ event.browse(alert['event_id'])._do_sms_reminder()
+ found = True
+ if found and not last_found: # if the precedent event had an alarm but not this one, we can stop the search for this event
+ break
+ else:
+ event_start = fields.Datetime.from_string(event.start)
+ for alert in self.do_check_alarm_for_one_date(event_start, event, max_delta, 0, 'sms', after=last_sms_cron, missing=True):
+ event.browse(alert['event_id'])._do_sms_reminder()
+ self.env['ir.config_parameter'].set_param('calendar_sms.last_sms_cron', now)
+ return result
diff --git a/addons/calendar_sms/views/calendar_views.xml b/addons/calendar_sms/views/calendar_views.xml
new file mode 100644
index 00000000000..4682a7098bf
--- /dev/null
+++ b/addons/calendar_sms/views/calendar_views.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/addons/sms/__init__.py b/addons/sms/__init__.py
new file mode 100644
index 00000000000..2ae6446f9dc
--- /dev/null
+++ b/addons/sms/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
+from . import wizard
diff --git a/addons/sms/__manifest__.py b/addons/sms/__manifest__.py
new file mode 100644
index 00000000000..18330c1d793
--- /dev/null
+++ b/addons/sms/__manifest__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+{
+ 'name': 'SMS gateway',
+ 'category': 'Tools',
+ 'summary': 'SMS Text Messaging',
+ 'description': """
+This module gives a framework for SMS text messaging
+----------------------------------------------------
+
+The service is provided by the In App Purchase Odoo platform.
+""",
+ 'depends': ['base', 'iap', 'mail'],
+ 'data': [
+ 'wizard/send_sms_views.xml',
+ 'views/res_partner_views.xml',
+ ],
+ 'installable': True,
+ 'auto_install': True,
+}
diff --git a/addons/sms/models/__init__.py b/addons/sms/models/__init__.py
new file mode 100644
index 00000000000..7a75a7a5f6d
--- /dev/null
+++ b/addons/sms/models/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import mail_thread
+from . import res_partner
+from . import sms_api
diff --git a/addons/sms/models/mail_thread.py b/addons/sms/models/mail_thread.py
new file mode 100644
index 00000000000..1fb62a72de4
--- /dev/null
+++ b/addons/sms/models/mail_thread.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+
+from odoo import models, _
+
+from odoo.addons.iap.models.iap import InsufficientCreditError
+
+_logger = logging.getLogger(__name__)
+
+
+class MailThread(models.AbstractModel):
+ _inherit = 'mail.thread'
+
+ def _get_default_sms_recipients(self):
+ """ This method will likely need to be overriden by inherited models.
+ :returns partners: recordset of res.partner
+ """
+ partners = self.env['res.partner']
+ if hasattr(self, 'partner_id'):
+ partners |= self.mapped('partner_id')
+ if hasattr(self, 'partner_ids'):
+ partners |= self.mapped('partner_ids')
+ return partners
+
+ def message_post_send_sms(self, sms_message, numbers=None, partners=None, note_msg=None, log_error=False):
+ """ Send an SMS text message and post an internal note in the chatter if successfull
+ :param sms_message: plaintext message to send by sms
+ :param partners: the numbers to send to, if none are given it will take those
+ from partners or _get_default_sms_recipients
+ :param partners: the recipients partners, if none are given it will take those
+ from _get_default_sms_recipients, this argument
+ is ignored if numbers is defined
+ :param note_msg: message to log in the chatter, if none is given a default one
+ containing the sms_message is logged
+ """
+ if not numbers:
+ if not partners:
+ partners = self._get_default_sms_recipients()
+
+ # Collect numbers, we will consider the message to be sent if at least one number can be found
+ numbers = list(set([i.mobile for i in partners if i.mobile]))
+
+ if numbers:
+ try:
+ self.env['sms.api']._send_sms(numbers, sms_message)
+ mail_message = note_msg or _('SMS message sent: %s') % sms_message
+
+ except InsufficientCreditError as e:
+ if not log_error:
+ raise e
+ mail_message = _('Insufficient credit, unable to send SMS message: %s') % sms_message
+ else:
+ mail_message = _('No mobile number defined, unable to send SMS message: %s') % sms_message
+
+ for thread in self:
+ thread.message_post(body=mail_message)
+ return False
diff --git a/addons/sms/models/res_partner.py b/addons/sms/models/res_partner.py
new file mode 100644
index 00000000000..6d85ef2fe06
--- /dev/null
+++ b/addons/sms/models/res_partner.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+
+
+class ResPartner(models.Model):
+ _inherit = 'res.partner'
+
+ def _get_default_sms_recipients(self):
+ """ Override of mail.thread method.
+ SMS recipients on partners are the partners themselves.
+ """
+ return self
diff --git a/addons/sms/models/sms_api.py b/addons/sms/models/sms_api.py
new file mode 100644
index 00000000000..1498ce54d98
--- /dev/null
+++ b/addons/sms/models/sms_api.py
@@ -0,0 +1,26 @@
+# -*- 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 UserError
+from odoo.addons.iap.models import iap
+
+DEFAULT_ENDPOINT = 'https://iap-sms.odoo.com'
+
+
+class SmsApi(models.AbstractModel):
+ _name = 'sms.api'
+
+ @api.model
+ def _send_sms(self, numbers, message):
+ """ Send sms
+ """
+ account = self.env['iap.account'].get('sms')
+ params = {
+ 'account_token': account.account_token,
+ 'numbers': numbers,
+ 'message': message,
+ }
+ endpoint = self.env['ir.config_parameter'].sudo().get_param('sms.endpoint', DEFAULT_ENDPOINT)
+ r = iap.jsonrpc(endpoint + '/iap/message_send', params=params)
+ return True
diff --git a/addons/sms/views/res_partner_views.xml b/addons/sms/views/res_partner_views.xml
new file mode 100644
index 00000000000..a35d5f10516
--- /dev/null
+++ b/addons/sms/views/res_partner_views.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ res.partner.form.send.sms
+ res.partner
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/sms/wizard/__init__.py b/addons/sms/wizard/__init__.py
new file mode 100644
index 00000000000..937147a0b43
--- /dev/null
+++ b/addons/sms/wizard/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import send_sms
diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py
new file mode 100644
index 00000000000..25fa0828934
--- /dev/null
+++ b/addons/sms/wizard/send_sms.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
+
+
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+from odoo.addons.iap.models import iap
+
+_logger = logging.getLogger(__name__)
+
+
+class SendSMS(models.TransientModel):
+ _name = 'sms.send_sms'
+
+ recipients = fields.Char('Recipients', required=True)
+ message = fields.Text('Message', required=True)
+
+ def _get_records(self, model):
+ if self.env.context.get('active_domain'):
+ records = model.search(self.env.context.get('active_domain'))
+ elif self.env.context.get('active_ids'):
+ records = model.browse(self.env.context.get('active_ids', []))
+ else:
+ records = model.browse(self.env.context.get('active_id', []))
+ return records
+
+ @api.model
+ def default_get(self, fields):
+ result = super(SendSMS, self).default_get(fields)
+
+ active_model = self.env.context.get('active_model')
+ model = self.env[active_model]
+
+ records = self._get_records(model)
+ if getattr(records, '_get_default_sms_recipients'):
+ partners = records._get_default_sms_recipients()
+ phone_numbers = []
+ no_phone_partners = []
+ for partner in records:
+ number = partner.mobile
+ if number:
+ phone_numbers.append(number)
+ else:
+ no_phone_partners.append(partner.name)
+ if len(partners) > 1:
+ if no_phone_partners:
+ raise UserError(_('Missing mobile number for %s.') % ', '.join(no_phone_partners))
+ result['recipients'] = ', '.join(phone_numbers)
+ return result
+
+ def action_send_sms(self):
+ numbers = self.recipients.split(',')
+
+ active_model = self.env.context.get('active_model')
+ model = self.env[active_model]
+ records = self._get_records(model)
+ if getattr(records, 'message_post_send_sms'):
+ records.message_post_send_sms(self.message, numbers=numbers)
+ else:
+ self.env['sms.api']._send_sms(numbers, self.message)
+ return True
diff --git a/addons/sms/wizard/send_sms_views.xml b/addons/sms/wizard/send_sms_views.xml
new file mode 100644
index 00000000000..34746c32825
--- /dev/null
+++ b/addons/sms/wizard/send_sms_views.xml
@@ -0,0 +1,44 @@
+
+
+
+
+ sms_send_sms.form
+ sms.send_sms
+
+
+
+
+
+ Send SMS
+ sms.send_sms
+ form
+ form
+ new
+
+
+
+
+
+