Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[16.0][MIG] sms_twilio: Migration to 16.0. #306

Open
wants to merge 3 commits into
base: 16.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# generated from manifests external_dependencies
phonenumbers
requests
twilio
1 change: 1 addition & 0 deletions setup/sms_twilio/odoo/addons/sms_twilio
6 changes: 6 additions & 0 deletions setup/sms_twilio/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
95 changes: 95 additions & 0 deletions sms_twilio/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
==========
SMS Twilio
==========

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:239b7212f3565c6315fe3c0326c1f5317f7a10d29dfa1712691d64284a7f1503
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fconnector--telephony-lightgray.png?logo=github
:target: https://github.com/OCA/connector-telephony/tree/16.0/sms_twilio
:alt: OCA/connector-telephony
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/connector-telephony-16-0/connector-telephony-16-0-sms_twilio
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/connector-telephony&target_branch=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

Implementation of **Twilio API** for sending sms.
This module depend of odoo native **sms** module it only implement Twilio as
provider.

**Table of contents**

.. contents::
:local:

Configuration
=============

To configure this module, you need to:

* Go to settings > technical > IAP Account
* Create a new account with **TWilio** as provider
* Fill your account information.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/connector-telephony/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/connector-telephony/issues/new?body=module:%20sms_twilio%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
~~~~~~~

* Akretion

Contributors
~~~~~~~~~~~~

* Maria de Luna <[email protected]>

Maintainers
~~~~~~~~~~~

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

.. |maintainer-mariadforgeflow| image:: https://github.com/mariadforgeflow.png?size=40px
:target: https://github.com/mariadforgeflow
:alt: mariadforgeflow

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-mariadforgeflow|

This module is part of the `OCA/connector-telephony <https://github.com/OCA/connector-telephony/tree/16.0/sms_twilio>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions sms_twilio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
22 changes: 22 additions & 0 deletions sms_twilio/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "SMS Twilio",
"summary": "Send sms using Twilio API",
"version": "16.0.1.0.0",
"category": "SMS",
"website": "https://github.com/OCA/connector-telephony",
"author": "Akretion, Odoo Community Association (OCA)",
"maintainers": ["mariadforgeflow"],
"license": "AGPL-3",
"application": False,
"installable": True,
"external_dependencies": {"python": ["twilio"], "bin": []},
"depends": ["base_phone", "sms", "iap_alternative_provider"],
"data": [
"views/iap_account_view.xml",
"views/sms_sms_view.xml",
"security/ir.model.access.csv",
],
}
4 changes: 4 additions & 0 deletions sms_twilio/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import iap_account
from . import sms_api
from . import sms_sms
from . import twilio_numbers
136 changes: 136 additions & 0 deletions sms_twilio/models/iap_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import logging

from odoo import _, api, fields, models
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)

try:
from twilio.rest import Client
except ImportError:
_logger.error("Cannot import twilio dependencies", exc_info=True)

Check warning on line 14 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L13-L14

Added lines #L13 - L14 were not covered by tests


class IapAccount(models.Model):
_inherit = "iap.account"

twilio_production_env = fields.Boolean(
"Production Environment",
help="Test credentials can currently be used to interact with"
" the following three resources:\n"
" - Buying phone numbers"
" - Sending SMS messages"
" - Making calls\n"
"If in test environment, although being able to perform the above actions,"
" they will not show in your Twilio account, and the only valid Twilio Number"
" is the TEST Phone.\n"
"Production keys are needed to perform all the other actions"
" like retrieve account balance or phone numbers."
" If this field is not enabled, the three cases mentioned above"
" will be performed using test credentials.",
)

provider = fields.Selection(
selection_add=[("twilio", "Twilio")],
ondelete={"twilio": "cascade"},
)

twilio_test_account_sid = fields.Char(string="Twilio TEST Account SID")
twilio_test_auth_token = fields.Char(string="Twilio TEST Auth Token")
twilio_account_sid = fields.Char(string="Twilio Account SID")
twilio_auth_token = fields.Char()
twilio_number_id = fields.Many2one("twilio.phone.number", string="Twilio Number")

twilio_balance_account = fields.Char(
string="Account Balance", compute="_compute_balance_twilio"
)

def _get_service_from_provider(self):
if self.provider == "twilio":
return "sms"
return super()._get_service_from_provider()

Check warning on line 54 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L53-L54

Added lines #L53 - L54 were not covered by tests

def _compute_balance_twilio(self):
for item in self:
balance = ""

Check warning on line 58 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L58

Added line #L58 was not covered by tests
if item.twilio_account_sid and item.twilio_auth_token:
try:

Check warning on line 60 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L60

Added line #L60 was not covered by tests
# Only work with prod creds
client = item.get_twilio_client()
balance_obj = client.api.balance.fetch()
balance = "%s: %s" % (balance_obj.currency, balance_obj.balance)
except Exception as e:
_logger.error("Twilio Error: '%s'", str(e))
item.twilio_balance_account = balance

Check warning on line 67 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L62-L67

Added lines #L62 - L67 were not covered by tests

def retrieve_phone_numbers(self):
for item in self:
if not item.twilio_account_sid or not item.twilio_auth_token:
raise UserError(_("Configure Twilio Credentials first"))
try:

Check warning on line 73 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L72-L73

Added lines #L72 - L73 were not covered by tests
# Only work with prod creds
client = item.get_twilio_client()
phone_numbers = client.incoming_phone_numbers.list()

Check warning on line 76 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L75-L76

Added lines #L75 - L76 were not covered by tests
for phone in phone_numbers:
if not self.env["twilio.phone.number"].search(
[("sid", "=", phone.sid)]
):
self.env["twilio.phone.number"].sudo().create(

Check warning on line 81 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L81

Added line #L81 was not covered by tests
{
"name": phone.friendly_name,
"phone_number": phone.phone_number,
"has_sms_enabled": phone.capabilities["sms"],
"sid": phone.sid,
}
)
if not (
self.twilio_production_env
and self.env["twilio.phone.number"].search([("sid", "=", "test")])
):
self.env["twilio.phone.number"].sudo().create(

Check warning on line 93 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L93

Added line #L93 was not covered by tests
{
"name": "TEST Phone",
"phone_number": "+15005550006",
"has_sms_enabled": True,
"sid": "test",
}
)

except Exception as e:
_logger.error("Twilio Error: '%s'", str(e))

Check warning on line 103 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L102-L103

Added lines #L102 - L103 were not covered by tests

def get_twilio_client(self, production=True):
if not self.twilio_account_sid or not self.twilio_auth_token:
raise UserError(_("Configure Twilio Credentials first"))

Check warning on line 107 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L107

Added line #L107 was not covered by tests
if not production:
return Client(self.twilio_test_account_sid, self.twilio_test_auth_token)
return Client(self.twilio_account_sid, self.twilio_auth_token)

Check warning on line 110 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L109-L110

Added lines #L109 - L110 were not covered by tests

@property
def _server_env_fields(self):
res = super()._server_env_fields
res.update(

Check warning on line 115 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L114-L115

Added lines #L114 - L115 were not covered by tests
{
"twilio_test_account_sid": {},
"twilio_test_auth_token": {},
"twilio_account_sid": {},
"twilio_auth_token": {},
}
)
return res

Check warning on line 123 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L123

Added line #L123 was not covered by tests

@api.onchange("twilio_production_env")
def onchange_twilio_production_env(self):
if self.twilio_production_env:
if not self.twilio_number_id or self.twilio_number_id == self.env[
"twilio.phone.number"
].search([("sid", "=", "test")]):
raise UserError(

Check warning on line 131 in sms_twilio/models/iap_account.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/iap_account.py#L131

Added line #L131 was not covered by tests
_(
"Select a valid Twilio Number before changing to Production"
" Environment."
)
)
68 changes: 68 additions & 0 deletions sms_twilio/models/sms_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import logging
import re

from odoo import _, api, models
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)

try:
from twilio.base.exceptions import TwilioRestException
except ImportError:
_logger.error("Cannot import twilio", exc_info=True)

Check warning on line 15 in sms_twilio/models/sms_api.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/sms_api.py#L14-L15

Added lines #L14 - L15 were not covered by tests


class SmsApi(models.AbstractModel):
_inherit = "sms.api"

def _get_twilio_sms_account(self):
return self.env["iap.account"].search(

Check warning on line 22 in sms_twilio/models/sms_api.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/sms_api.py#L22

Added line #L22 was not covered by tests
[("provider", "=", "twilio"), ("service_name", "=", "sms")]
)

def _send_sms_with_twilio(self, number, message, sms_id):
# Try to return same error code like odoo
# list is here: self.IAP_TO_SMS_STATE
if not number:
return "wrong_number_format"
try:
account = self._get_twilio_sms_account()
client = account.get_twilio_client(production=account.twilio_production_env)
from_phone = account.twilio_number_id
from_number = "+" + re.sub("[^0-9]", "", from_phone.phone_number)
number = "+" + re.sub("[^0-9]", "", number)
client.messages.create(to=number, from_=from_number, body=message)
except TwilioRestException as e:
self.env["sms.sms"].browse(sms_id).error_detail = e.msg
raise UserError(e.msg) from e
return "success"

Check warning on line 41 in sms_twilio/models/sms_api.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/sms_api.py#L30-L41

Added lines #L30 - L41 were not covered by tests

@api.model
def _send_sms(self, numbers, message):
if self._get_twilio_sms_account():
# This method seem to be deprecated (no odoo code use it)
# as Twilio do not support it we do not support it
# Note: if you want to implement it becareful just looping
# on the list of number is not the right way to do it.
# If you have an error, you will send and send again the same
# message
raise NotImplementedError

Check warning on line 52 in sms_twilio/models/sms_api.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/sms_api.py#L52

Added line #L52 was not covered by tests
else:
return super()._send_sms(numbers, message)

Check warning on line 54 in sms_twilio/models/sms_api.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/sms_api.py#L54

Added line #L54 was not covered by tests

@api.model
def _send_sms_batch(self, messages):
if self._get_twilio_sms_account():
if len(messages) != 1:
# we already have inherited the split_batch method on sms.sms
# so this case should not append
raise UserError(_("Batch sending is not supported with Twilio"))
state = self._send_sms_with_twilio(

Check warning on line 63 in sms_twilio/models/sms_api.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/sms_api.py#L62-L63

Added lines #L62 - L63 were not covered by tests
messages[0]["number"], messages[0]["content"], messages[0]["res_id"]
)
return [{"state": state, "credit": 0, "res_id": messages[0]["res_id"]}]

Check warning on line 66 in sms_twilio/models/sms_api.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/sms_api.py#L66

Added line #L66 was not covered by tests
else:
return super()._send_sms_batch(messages)

Check warning on line 68 in sms_twilio/models/sms_api.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/sms_api.py#L68

Added line #L68 was not covered by tests
18 changes: 18 additions & 0 deletions sms_twilio/models/sms_sms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import fields, models


class SmsSms(models.Model):
_inherit = "sms.sms"

error_detail = fields.Text(readonly=True)

def _split_batch(self):
if self.env["sms.api"]._get_twilio_sms_account():
# No batch with Twilio
for record in self:
yield [record.id]

Check warning on line 16 in sms_twilio/models/sms_sms.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/sms_sms.py#L16

Added line #L16 was not covered by tests
else:
yield from super()._split_batch()

Check warning on line 18 in sms_twilio/models/sms_sms.py

View check run for this annotation

Codecov / codecov/patch

sms_twilio/models/sms_sms.py#L18

Added line #L18 was not covered by tests
28 changes: 28 additions & 0 deletions sms_twilio/models/twilio_numbers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com).
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).


from odoo import fields, models


class TwilioPhoneNumber(models.Model):
_name = "twilio.phone.number"
_description = "Twilio Phone Number"

name = fields.Char(string="Friendly Name")
phone_number = fields.Char()
has_sms_enabled = fields.Boolean(string="SMS Enabled")
sid = fields.Char(string="SID")

_sql_constraints = [
(
"uniq_phone_sid",
"unique(sid)",
"You already have this sid used in other record",
),
(
"uniq_phone_number",
"unique(phone_number)",
"You already have this phone number used in other record",
),
]
Loading
Loading