Skip to content
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
74 changes: 74 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
exclude: 'node_modules|.git'
default_stages: [commit]
fail_fast: false


repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: trailing-whitespace
files: "erpnext.*"
exclude: ".*json$|.*txt$|.*csv|.*md"
- id: check-yaml
- id: no-commit-to-branch
args: ['--branch', 'develop']
- id: check-merge-conflict
- id: check-ast
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements

- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
- id: prettier
types_or: [javascript, vue, scss]
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
erpnext/public/dist/.*|
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/public/js/controllers/.*|
erpnext/templates/pages/order.js|
erpnext/templates/includes/.*
)$

- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.44.0
hooks:
- id: eslint
types_or: [javascript]
args: ['--quiet']
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
erpnext/public/dist/.*|
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/public/js/controllers/.*|
erpnext/templates/pages/order.js|
erpnext/templates/includes/.*
)$

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.0
hooks:
- id: ruff
name: "Run ruff import sorter"
args: ["--select=I", "--fix"]

- id: ruff
name: "Run ruff linter"

- id: ruff-format
name: "Run ruff formatter"

ci:
autoupdate_schedule: weekly
skip: []
submodules: false
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"fieldname": "whatsapp_no",
"fieldtype": "Data",
"label": "Number",
"mandatory_depends_on": "eval: doc.enabled",
"options": "Phone"
},
{
Expand All @@ -108,7 +109,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-07-08 00:46:42.317641",
"modified": "2025-02-28 13:40:12.974554",
"modified_by": "Administrator",
"module": "Twilio Integration",
"name": "Twilio Settings",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,91 +3,100 @@
# For license information, please see license.txt

from __future__ import unicode_literals

import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils.password import get_decrypted_password
from frappe.model.document import Document
from twilio.rest import Client

from six import string_types
import re
from json import loads, dumps
from random import randrange
from ...utils import get_public_url, validate_phone_number

from twilio.rest import Client
from ...utils import get_public_url

class TwilioSettings(Document):
friendly_resource_name = "ERPNext" # System creates TwiML app & API keys with this name.

def validate(self):
self.validate_twilio_account()

def on_update(self):
# Single doctype records are created in DB at time of installation and those field values are set as null.
# This condition make sure that we handle null.
if not self.account_sid:
return

twilio = Client(self.account_sid, self.get_password("auth_token"))
self.set_api_credentials(twilio)
self.set_application_credentials(twilio)
self.reload()

def validate_twilio_account(self):
try:
twilio = Client(self.account_sid, self.get_password("auth_token"))
twilio.api.accounts(self.account_sid).fetch()
return twilio
except Exception:
frappe.throw(_("Invalid Account SID or Auth Token."))

def set_api_credentials(self, twilio):
"""Generate Twilio API credentials if not exist and update them.
"""
if self.api_key and self.api_secret:
return
new_key = self.create_api_key(twilio)
self.api_key = new_key.sid
self.api_secret = new_key.secret
frappe.db.set_value('Twilio Settings', 'Twilio Settings', {
'api_key': self.api_key,
'api_secret': self.api_secret
})

def set_application_credentials(self, twilio):
"""Generate TwiML app credentials if not exist and update them.
"""
credentials = self.get_application(twilio) or self.create_application(twilio)
self.twiml_sid = credentials.sid
frappe.db.set_value('Twilio Settings', 'Twilio Settings', 'twiml_sid', self.twiml_sid)

def create_api_key(self, twilio):
"""Create API keys in twilio account.
"""
try:
return twilio.new_keys.create(friendly_name=self.friendly_resource_name)
except Exception:
frappe.log_error(title=_("Twilio API credential creation error."))
frappe.throw(_("Twilio API credential creation error."))

def get_twilio_voice_url(self):
url_path = "/api/method/twilio_integration.twilio_integration.api.voice"
return get_public_url(url_path)

def get_application(self, twilio, friendly_name=None):
"""Get TwiML App from twilio account if exists.
"""
friendly_name = friendly_name or self.friendly_resource_name
applications = twilio.applications.list(friendly_name)
return applications and applications[0]

def create_application(self, twilio, friendly_name=None):
"""Create TwilML App in twilio account.
"""
friendly_name = friendly_name or self.friendly_resource_name
application = twilio.applications.create(
voice_method='POST',
voice_url=self.get_twilio_voice_url(),
friendly_name=friendly_name
)
return application
friendly_resource_name = (
"ERPNext" # System creates TwiML app & API keys with this name.
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._twilio_client = None

def validate(self):
if not self.enabled:
return
self.validate_whatsapp_number()
self.validate_twilio_account()

def on_update(self):
# Single doctype records are created in DB at time of installation and those field values are set as null.
# This condition make sure that we handle null.
self.update_twilio_account()

def validate_whatsapp_number(self):
validate_phone_number(self.whatsapp_no)

def update_twilio_account(self):
twilio = self.get_twilio_client()

if not (self.api_key and self.api_secret):
self.set_api_credentials(twilio)

self.set_application_credentials(twilio)
self.reload()

def get_twilio_client(self):
if not self._twilio_client:
self._twilio_client = Client(
self.account_sid, self.get_password("auth_token")
)
return self._twilio_client

def validate_twilio_account(self):
try:
twilio = self.get_twilio_client()
twilio.api.accounts(self.account_sid).fetch()
except Exception:
frappe.throw(_("Invalid Account SID or Auth Token."))

def set_api_credentials(self, twilio):
"""Generate Twilio API credentials if not exist and update them."""
new_key = self.create_api_key(twilio)
self.api_key = new_key.sid
self.api_secret = new_key.secret
frappe.db.set_single_value(
"Twilio Settings", {"api_key": self.api_key, "api_secret": self.api_secret}
)

def set_application_credentials(self, twilio):
"""Generate TwiML app credentials if not exist and update them."""
credentials = self.get_application(twilio) or self.create_application(twilio)
self.twiml_sid = credentials.sid
frappe.db.set_single_value("Twilio Settings", "twiml_sid", self.twiml_sid)

def create_api_key(self, twilio):
"""Create API keys in twilio account."""
try:
return twilio.new_keys.create(friendly_name=self.friendly_resource_name)
except Exception:
frappe.log_error(title=_("Twilio API credential creation error."))
frappe.throw(_("Twilio API credential creation error."))

def get_twilio_voice_url(self):
url_path = "/api/method/twilio_integration.twilio_integration.api.voice"
return get_public_url(url_path)

def get_application(self, twilio, friendly_name=None):
"""Get TwiML App from twilio account if exists."""
friendly_name = friendly_name or self.friendly_resource_name
applications = twilio.applications.list(friendly_name)
return applications and applications[0]

def create_application(self, twilio, friendly_name=None):
"""Create TwilML App in twilio account."""
friendly_name = friendly_name or self.friendly_resource_name
application = twilio.applications.create(
voice_method="POST",
voice_url=self.get_twilio_voice_url(),
friendly_name=friendly_name,
)
return application
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,21 @@
// For license information, please see license.txt

frappe.ui.form.on('WhatsApp Campaign', {
setup: function(frm) {
onload: function(frm) {
frappe.call({
doc: frm.doc,
method: 'get_doctype_list',
callback: function(r) {
if(r.message) {
let options = []
r.message.forEach((dt) => {
options.push({
'label': dt,
'value': dt
});
})
frappe.meta.get_docfield('WhatsApp Campaign Recipient', 'campaign_for', frm.doc.name).options = [""].concat(options);
frappe.meta.get_docfield('WhatsApp Campaign Recipient', 'campaign_for', frm.doc.name).options = [""].concat(r.message);
}
frm.fields_dict.recipients.grid.add_new_row()
}
});
},

refresh: function(frm) {
frm.set_df_property("recipients", "reqd", 1);
if(frm.doc.status == 'Completed') {
frm.disable_form();
frm.disable_save();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
{
"fieldname": "total_participants",
"fieldtype": "Int",
"label": "Total Participants"
"label": "Total Participants",
"read_only": 1
},
{
"fieldname": "campaign",
Expand All @@ -80,8 +81,7 @@
"fieldname": "recipients",
"fieldtype": "Table",
"label": "Recipients",
"options": "WhatsApp Campaign Recipient",
"reqd": 1
"options": "WhatsApp Campaign Recipient"
},
{
"fieldname": "messge_section",
Expand Down Expand Up @@ -111,7 +111,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-07-14 11:40:59.599233",
"modified": "2025-02-28 16:22:54.783961",
"modified_by": "Administrator",
"module": "Twilio Integration",
"name": "WhatsApp Campaign",
Expand Down
Loading