1
0
forked from test/crm

Merge pull request #534 from shariquerik/telephony

This commit is contained in:
Shariq Ansari 2025-01-19 18:03:18 +05:30 committed by GitHub
commit a242ef162a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 303 additions and 202 deletions

View File

@ -5,7 +5,16 @@ import frappe
def get_users(): def get_users():
users = frappe.qb.get_query( users = frappe.qb.get_query(
"User", "User",
fields=["name", "email", "enabled", "user_image", "first_name", "last_name", "full_name", "user_type"], fields=[
"name",
"email",
"enabled",
"user_image",
"first_name",
"last_name",
"full_name",
"user_type",
],
order_by="full_name asc", order_by="full_name asc",
distinct=True, distinct=True,
).run(as_dict=1) ).run(as_dict=1)
@ -14,11 +23,13 @@ def get_users():
if frappe.session.user == user.name: if frappe.session.user == user.name:
user.session_user = True user.session_user = True
user.is_manager = ( user.is_manager = "Sales Manager" in frappe.get_roles(user.name) or user.name == "Administrator"
"Sales Manager" in frappe.get_roles(user.name) or user.name == "Administrator"
) user.is_agent = frappe.db.exists("CRM Telephony Agent", {"user": user.name})
return users return users
@frappe.whitelist() @frappe.whitelist()
def get_contacts(): def get_contacts():
contacts = frappe.get_all( contacts = frappe.get_all(
@ -37,7 +48,7 @@ def get_contacts():
"mobile_no", "mobile_no",
"phone", "phone",
"company_name", "company_name",
"modified" "modified",
], ],
order_by="first_name asc", order_by="first_name asc",
distinct=True, distinct=True,
@ -58,18 +69,12 @@ def get_contacts():
return contacts return contacts
@frappe.whitelist() @frappe.whitelist()
def get_lead_contacts(): def get_lead_contacts():
lead_contacts = frappe.get_all( lead_contacts = frappe.get_all(
"CRM Lead", "CRM Lead",
fields=[ fields=["name", "lead_name", "mobile_no", "phone", "image", "modified"],
"name",
"lead_name",
"mobile_no",
"phone",
"image",
"modified"
],
filters={"converted": 0}, filters={"converted": 0},
order_by="lead_name asc", order_by="lead_name asc",
distinct=True, distinct=True,
@ -77,11 +82,12 @@ def get_lead_contacts():
return lead_contacts return lead_contacts
@frappe.whitelist() @frappe.whitelist()
def get_organizations(): def get_organizations():
organizations = frappe.qb.get_query( organizations = frappe.qb.get_query(
"CRM Organization", "CRM Organization",
fields=['*'], fields=["*"],
order_by="name asc", order_by="name asc",
distinct=True, distinct=True,
).run(as_dict=1) ).run(as_dict=1)

View File

@ -1,7 +1,7 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
// frappe.ui.form.on("CRM Exotel Agent", { // frappe.ui.form.on("CRM Telephony Agent", {
// refresh(frm) { // refresh(frm) {
// }, // },

View File

@ -1,7 +1,7 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:mobile_no", "autoname": "field:user",
"creation": "2025-01-11 16:12:46.602782", "creation": "2025-01-11 16:12:46.602782",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
@ -10,7 +10,15 @@
"user_name", "user_name",
"column_break_hdec", "column_break_hdec",
"mobile_no", "mobile_no",
"exotel_number" "default_medium",
"section_break_ozjn",
"twilio",
"twilio_number",
"column_break_aydj",
"exotel",
"exotel_number",
"section_break_phlq",
"phone_nos"
], ],
"fields": [ "fields": [
{ {
@ -20,7 +28,8 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "User", "label": "User",
"options": "User", "options": "User",
"reqd": 1 "reqd": 1,
"unique": 1
}, },
{ {
"fieldname": "column_break_hdec", "fieldname": "column_break_hdec",
@ -32,8 +41,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Mobile No.", "label": "Mobile No.",
"reqd": 1, "read_only": 1
"unique": 1
}, },
{ {
"fetch_from": "user.full_name", "fetch_from": "user.full_name",
@ -44,19 +52,62 @@
"label": "User Name" "label": "User Name"
}, },
{ {
"depends_on": "exotel",
"fieldname": "exotel_number", "fieldname": "exotel_number",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "label": "Exotel Number",
"in_standard_filter": 1, "mandatory_depends_on": "exotel"
"label": "Exotel Number" },
{
"fieldname": "section_break_phlq",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_ozjn",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_aydj",
"fieldtype": "Column Break"
},
{
"depends_on": "twilio",
"fieldname": "twilio_number",
"fieldtype": "Data",
"label": "Twilio Number",
"mandatory_depends_on": "twilio"
},
{
"fieldname": "phone_nos",
"fieldtype": "Table",
"label": "Phone Numbers",
"options": "CRM Telephony Phone"
},
{
"default": "0",
"fieldname": "twilio",
"fieldtype": "Check",
"label": "Twilio"
},
{
"default": "0",
"fieldname": "exotel",
"fieldtype": "Check",
"label": "Exotel"
},
{
"fieldname": "default_medium",
"fieldtype": "Select",
"label": "Default Medium",
"options": "\nTwilio\nExotel"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-01-15 20:03:31.162162", "modified": "2025-01-19 14:17:12.880185",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Exotel Agent", "name": "CRM Telephony Agent",
"naming_rule": "By fieldname", "naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@ -0,0 +1,34 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class CRMTelephonyAgent(Document):
def validate(self):
self.set_primary()
def set_primary(self):
# Used to set primary mobile no.
if len(self.phone_nos) == 0:
self.mobile_no = ""
return
is_primary = [phone.number for phone in self.phone_nos if phone.get("is_primary")]
if len(is_primary) > 1:
frappe.throw(
_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub("mobile_no")))
)
primary_number_exists = False
for d in self.phone_nos:
if d.get("is_primary") == 1:
primary_number_exists = True
self.mobile_no = d.number
break
if not primary_number_exists:
self.mobile_no = ""

View File

@ -4,7 +4,6 @@
# import frappe # import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all # On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded # link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list # Use these module variables to add/remove to/from that list
@ -12,18 +11,18 @@ EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestCRMExotelAgent(UnitTestCase): class UnitTestCRMTelephonyAgent(UnitTestCase):
""" """
Unit tests for CRMExotelAgent. Unit tests for CRMTelephonyAgent.
Use this class for testing individual functions and methods. Use this class for testing individual functions and methods.
""" """
pass pass
class IntegrationTestCRMExotelAgent(IntegrationTestCase): class IntegrationTestCRMTelephonyAgent(IntegrationTestCase):
""" """
Integration tests for CRMExotelAgent. Integration tests for CRMTelephonyAgent.
Use this class for testing interactions between multiple components. Use this class for testing interactions between multiple components.
""" """

View File

@ -0,0 +1,40 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-01-19 13:57:01.702519",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"number",
"is_primary"
],
"fields": [
{
"fieldname": "number",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Number",
"reqd": 1
},
{
"default": "0",
"fieldname": "is_primary",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Primary"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-01-19 13:58:59.063775",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Telephony Phone",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -5,5 +5,5 @@
from frappe.model.document import Document from frappe.model.document import Document
class CRMExotelAgent(Document): class CRMTelephonyPhone(Document):
pass pass

View File

@ -1,7 +1,7 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
// frappe.ui.form.on("Twilio Settings", { // frappe.ui.form.on("CRM Twilio Settings", {
// refresh(frm) { // refresh(frm) {
// }, // },

View File

@ -108,7 +108,7 @@
"modified": "2025-01-15 19:35:13.406254", "modified": "2025-01-15 19:35:13.406254",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "Twilio Settings", "name": "CRM Twilio Settings",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@ -2,13 +2,13 @@
# For license information, please see license.txt # For license information, please see license.txt
import frappe import frappe
from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.model.document import Document
from twilio.rest import Client from twilio.rest import Client
class TwilioSettings(Document):
friendly_resource_name = "Frappe CRM" # System creates TwiML app & API keys with this name. class CRMTwilioSettings(Document):
friendly_resource_name = "Frappe CRM" # System creates TwiML app & API keys with this name.
def validate(self): def validate(self):
self.validate_twilio_account() self.validate_twilio_account()
@ -33,28 +33,26 @@ class TwilioSettings(Document):
frappe.throw(_("Invalid Account SID or Auth Token.")) frappe.throw(_("Invalid Account SID or Auth Token."))
def set_api_credentials(self, twilio): def set_api_credentials(self, twilio):
"""Generate Twilio API credentials if not exist and update them. """Generate Twilio API credentials if not exist and update them."""
"""
if self.api_key and self.api_secret: if self.api_key and self.api_secret:
return return
new_key = self.create_api_key(twilio) new_key = self.create_api_key(twilio)
self.api_key = new_key.sid self.api_key = new_key.sid
self.api_secret = new_key.secret self.api_secret = new_key.secret
frappe.db.set_value('Twilio Settings', 'Twilio Settings', { frappe.db.set_value(
'api_key': self.api_key, "CRM Twilio Settings",
'api_secret': self.api_secret "CRM Twilio Settings",
}) {"api_key": self.api_key, "api_secret": self.api_secret},
)
def set_application_credentials(self, twilio): def set_application_credentials(self, twilio):
"""Generate TwiML app credentials if not exist and update them. """Generate TwiML app credentials if not exist and update them."""
"""
credentials = self.get_application(twilio) or self.create_application(twilio) credentials = self.get_application(twilio) or self.create_application(twilio)
self.twiml_sid = credentials.sid self.twiml_sid = credentials.sid
frappe.db.set_value('Twilio Settings', 'Twilio Settings', 'twiml_sid', self.twiml_sid) frappe.db.set_value("CRM Twilio Settings", "CRM Twilio Settings", "twiml_sid", self.twiml_sid)
def create_api_key(self, twilio): def create_api_key(self, twilio):
"""Create API keys in twilio account. """Create API keys in twilio account."""
"""
try: try:
return twilio.new_keys.create(friendly_name=self.friendly_resource_name) return twilio.new_keys.create(friendly_name=self.friendly_resource_name)
except Exception: except Exception:
@ -66,23 +64,21 @@ class TwilioSettings(Document):
return get_public_url(url_path) return get_public_url(url_path)
def get_application(self, twilio, friendly_name=None): def get_application(self, twilio, friendly_name=None):
"""Get TwiML App from twilio account if exists. """Get TwiML App from twilio account if exists."""
"""
friendly_name = friendly_name or self.friendly_resource_name friendly_name = friendly_name or self.friendly_resource_name
applications = twilio.applications.list(friendly_name) applications = twilio.applications.list(friendly_name)
return applications and applications[0] return applications and applications[0]
def create_application(self, twilio, friendly_name=None): def create_application(self, twilio, friendly_name=None):
"""Create TwilML App in twilio account. """Create TwilML App in twilio account."""
"""
friendly_name = friendly_name or self.friendly_resource_name friendly_name = friendly_name or self.friendly_resource_name
application = twilio.applications.create( application = twilio.applications.create(
voice_method='POST', voice_method="POST", voice_url=self.get_twilio_voice_url(), friendly_name=friendly_name
voice_url=self.get_twilio_voice_url(), )
friendly_name=friendly_name
)
return application return application
def get_public_url(path: str=None):
def get_public_url(path: str | None = None):
from frappe.utils import get_url from frappe.utils import get_url
return get_url().split(":8", 1)[0] + path
return get_url().split(":8", 1)[0] + path

View File

@ -5,5 +5,5 @@
from frappe.tests import UnitTestCase from frappe.tests import UnitTestCase
class TestTwilioAgents(UnitTestCase): class TestCRMTwilioSettings(UnitTestCase):
pass pass

View File

@ -12,9 +12,7 @@
"brand_logo", "brand_logo",
"favicon", "favicon",
"dropdown_items_tab", "dropdown_items_tab",
"dropdown_items", "dropdown_items"
"calling_tab",
"default_calling_medium"
], ],
"fields": [ "fields": [
{ {
@ -58,23 +56,12 @@
"fieldname": "favicon", "fieldname": "favicon",
"fieldtype": "Attach", "fieldtype": "Attach",
"label": "Favicon" "label": "Favicon"
},
{
"fieldname": "calling_tab",
"fieldtype": "Tab Break",
"label": "Calling"
},
{
"fieldname": "default_calling_medium",
"fieldtype": "Select",
"label": "Default calling medium",
"options": "\nTwilio\nExotel"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-01-15 17:40:32.784762", "modified": "2025-01-19 14:23:05.981355",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "FCRM Settings", "name": "FCRM Settings",

View File

@ -1,8 +0,0 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Twilio Agents", {
// refresh(frm) {
// },
// });

View File

@ -1,78 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:user",
"creation": "2023-08-17 19:59:56.239729",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user",
"user_name",
"call_receiving_device",
"column_break_ljne",
"twilio_number"
],
"fields": [
{
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User",
"unique": 1
},
{
"fieldname": "column_break_ljne",
"fieldtype": "Column Break"
},
{
"fieldname": "twilio_number",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Twilio Number",
"options": "Phone"
},
{
"fetch_from": "user.full_name",
"fieldname": "user_name",
"fieldtype": "Data",
"label": "User Name",
"read_only": 1
},
{
"default": "Computer",
"fieldname": "call_receiving_device",
"fieldtype": "Select",
"label": "Device",
"options": "Computer\nPhone"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-01-19 21:57:18.626669",
"modified_by": "Administrator",
"module": "FCRM",
"name": "Twilio Agents",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class TwilioAgents(Document):
pass

View File

@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestTwilioSettings(UnitTestCase):
pass

View File

@ -7,20 +7,42 @@ from crm.utils import are_same_phone_number, parse_phone_number
@frappe.whitelist() @frappe.whitelist()
def is_call_integration_enabled(): def is_call_integration_enabled():
twilio_enabled = frappe.db.get_single_value("Twilio Settings", "enabled") twilio_enabled = frappe.db.get_single_value("CRM Twilio Settings", "enabled")
exotel_enabled = frappe.db.get_single_value("CRM Exotel Settings", "enabled") exotel_enabled = frappe.db.get_single_value("CRM Exotel Settings", "enabled")
default_calling_medium = frappe.db.get_single_value("FCRM Settings", "default_calling_medium")
return { return {
"twilio_enabled": twilio_enabled, "twilio_enabled": twilio_enabled,
"exotel_enabled": exotel_enabled, "exotel_enabled": exotel_enabled,
"default_calling_medium": default_calling_medium, "default_calling_medium": get_user_default_calling_medium(),
} }
def get_user_default_calling_medium():
if not frappe.db.exists("CRM Telephony Agent", frappe.session.user):
return None
default_medium = frappe.db.get_value("CRM Telephony Agent", frappe.session.user, "default_medium")
if not default_medium:
return None
return default_medium
@frappe.whitelist() @frappe.whitelist()
def set_default_calling_medium(medium): def set_default_calling_medium(medium):
return frappe.db.set_value("FCRM Settings", "FCRM Settings", "default_calling_medium", medium) if not frappe.db.exists("CRM Telephony Agent", frappe.session.user):
frappe.get_doc(
{
"doctype": "CRM Telephony Agent",
"agent": frappe.session.user,
"default_medium": medium,
}
).insert(ignore_permissions=True)
else:
frappe.db.set_value("CRM Telephony Agent", frappe.session.user, "default_medium", medium)
return get_user_default_calling_medium()
@frappe.whitelist() @frappe.whitelist()

View File

@ -67,17 +67,22 @@ def make_a_call(to_number, from_number=None, caller_id=None):
endpoint = get_exotel_endpoint("Calls/connect.json?details=true") endpoint = get_exotel_endpoint("Calls/connect.json?details=true")
if not from_number: if not from_number:
from_number = frappe.get_value("CRM Exotel Agent", {"user": frappe.session.user}, "mobile_no") from_number = frappe.get_value("CRM Telephony Agent", {"user": frappe.session.user}, "mobile_no")
if not caller_id: if not caller_id:
caller_id = frappe.get_value("CRM Exotel Agent", {"user": frappe.session.user}, "exotel_number") caller_id = frappe.get_value("CRM Telephony Agent", {"user": frappe.session.user}, "exotel_number")
if not caller_id:
frappe.throw(
_("You do not have Exotel Number set in your Telephony Agent"), title=_("Exotel Number Missing")
)
if caller_id and caller_id not in get_all_exophones(): if caller_id and caller_id not in get_all_exophones():
frappe.throw(_("Exotel Number {0} is not valid").format(caller_id), title=_("Invalid Exotel Number")) frappe.throw(_("Exotel Number {0} is not valid").format(caller_id), title=_("Invalid Exotel Number"))
if not from_number: if not from_number:
frappe.throw( frappe.throw(
_("You do not have mobile number set in your Exotel Agent"), title=_("Mobile Number Missing") _("You do not have mobile number set in your Telephony Agent"), title=_("Mobile Number Missing")
) )
record_call = frappe.db.get_single_value("CRM Exotel Settings", "record_call") record_call = frappe.db.get_single_value("CRM Exotel Settings", "record_call")

View File

@ -11,7 +11,7 @@ from .twilio_handler import IncomingCall, Twilio, TwilioCallDetails
@frappe.whitelist() @frappe.whitelist()
def is_enabled(): def is_enabled():
return frappe.db.get_single_value("Twilio Settings", "enabled") return frappe.db.get_single_value("CRM Twilio Settings", "enabled")
@frappe.whitelist() @frappe.whitelist()

View File

@ -14,7 +14,7 @@ class Twilio:
def __init__(self, settings): def __init__(self, settings):
""" """
:param settings: `Twilio Settings` doctype :param settings: `CRM Twilio Settings` doctype
""" """
self.settings = settings self.settings = settings
self.account_sid = settings.account_sid self.account_sid = settings.account_sid
@ -26,7 +26,7 @@ class Twilio:
@classmethod @classmethod
def connect(self): def connect(self):
"""Make a twilio connection.""" """Make a twilio connection."""
settings = frappe.get_doc("Twilio Settings") settings = frappe.get_doc("CRM Twilio Settings")
if not (settings and settings.enabled): if not (settings and settings.enabled):
return return
return Twilio(settings=settings) return Twilio(settings=settings)
@ -114,11 +114,11 @@ class Twilio:
@classmethod @classmethod
def get_twilio_client(self): def get_twilio_client(self):
twilio_settings = frappe.get_doc("Twilio Settings") twilio_settings = frappe.get_doc("CRM Twilio Settings")
if not twilio_settings.enabled: if not twilio_settings.enabled:
frappe.throw(_("Please enable twilio settings before making a call.")) frappe.throw(_("Please enable twilio settings before making a call."))
auth_token = get_decrypted_password("Twilio Settings", "Twilio Settings", "auth_token") auth_token = get_decrypted_password("CRM Twilio Settings", "CRM Twilio Settings", "auth_token")
client = TwilioClient(twilio_settings.account_sid, auth_token) client = TwilioClient(twilio_settings.account_sid, auth_token)
return client return client

View File

@ -2,6 +2,7 @@
# Patches added in this section will be executed before doctypes are migrated # Patches added in this section will be executed before doctypes are migrated
# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations # Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
crm.patches.v1_0.move_crm_note_data_to_fcrm_note crm.patches.v1_0.move_crm_note_data_to_fcrm_note
crm.patches.v1_0.rename_twilio_settings_to_crm_twilio_settings
[post_model_sync] [post_model_sync]
# Patches added in this section will be executed after doctypes are migrated # Patches added in this section will be executed after doctypes are migrated
@ -9,4 +10,5 @@ crm.patches.v1_0.create_email_template_custom_fields
crm.patches.v1_0.create_default_fields_layout #10/12/2024 crm.patches.v1_0.create_default_fields_layout #10/12/2024
crm.patches.v1_0.create_default_sidebar_fields_layout crm.patches.v1_0.create_default_sidebar_fields_layout
crm.patches.v1_0.update_deal_quick_entry_layout crm.patches.v1_0.update_deal_quick_entry_layout
crm.patches.v1_0.update_layouts_to_new_format crm.patches.v1_0.update_layouts_to_new_format
crm.patches.v1_0.move_twilio_agent_to_telephony_agent

View File

@ -0,0 +1,27 @@
import frappe
def execute():
if not frappe.db.exists("DocType", "CRM Telephony Agent"):
frappe.reload_doctype("CRM Telephony Agent", force=True)
if frappe.db.exists("DocType", "Twilio Agents") and frappe.db.count("Twilio Agents") == 0:
return
agents = frappe.db.sql("SELECT * FROM `tabTwilio Agents`", as_dict=True)
if agents:
for agent in agents:
doc = frappe.get_doc(
{
"doctype": "CRM Telephony Agent",
"creation": agent.get("creation"),
"modified": agent.get("modified"),
"modified_by": agent.get("modified_by"),
"owner": agent.get("owner"),
"user": agent.get("user"),
"twilio_number": agent.get("twilio_number"),
"user_name": agent.get("user_name"),
"twilio": True,
}
)
doc.db_insert()

View File

@ -0,0 +1,20 @@
import frappe
from frappe.model.rename_doc import rename_doc
def execute():
if frappe.db.exists("DocType", "Twilio Settings"):
frappe.flags.ignore_route_conflict_validation = True
rename_doc("DocType", "Twilio Settings", "CRM Twilio Settings")
frappe.flags.ignore_route_conflict_validation = False
frappe.reload_doctype("CRM Twilio Settings", force=True)
if frappe.db.exists("__Auth", {"doctype": "Twilio Settings"}):
Auth = frappe.qb.DocType("__Auth")
result = frappe.qb.from_(Auth).select("*").where(Auth.doctype == "Twilio Settings").run(as_dict=True)
for row in result:
frappe.qb.into(Auth).insert(
"CRM Twilio Settings", "CRM Twilio Settings", row.fieldname, row.password, row.encrypted
).run()

View File

@ -67,7 +67,7 @@ import {
import { Dialog, Button, Avatar } from 'frappe-ui' import { Dialog, Button, Avatar } from 'frappe-ui'
import { ref, markRaw, computed, watch, h } from 'vue' import { ref, markRaw, computed, watch, h } from 'vue'
const { isManager, getUser } = usersStore() const { isManager, isAgent, getUser } = usersStore()
const user = computed(() => getUser() || {}) const user = computed(() => getUser() || {})
@ -108,20 +108,22 @@ const tabs = computed(() => {
label: __('Telephony'), label: __('Telephony'),
icon: PhoneIcon, icon: PhoneIcon,
component: markRaw(TelephonySettings), component: markRaw(TelephonySettings),
condition: () => isManager() || isAgent(),
}, },
{ {
label: __('WhatsApp'), label: __('WhatsApp'),
icon: WhatsAppIcon, icon: WhatsAppIcon,
component: markRaw(WhatsAppSettings), component: markRaw(WhatsAppSettings),
condition: () => isWhatsappInstalled.value, condition: () => isWhatsappInstalled.value && isManager(),
}, },
{ {
label: __('ERPNext'), label: __('ERPNext'),
icon: ERPNextIcon, icon: ERPNextIcon,
component: markRaw(ERPNextSettings), component: markRaw(ERPNextSettings),
condition: () => isManager(),
}, },
], ],
condition: () => isManager(), condition: () => isManager() || isAgent(),
}, },
] ]

View File

@ -19,17 +19,18 @@
<FormControl <FormControl
type="select" type="select"
v-model="defaultCallingMedium" v-model="defaultCallingMedium"
:label="__('Default calling medium')" :label="__('Default medium')"
:options="[ :options="[
{ label: __(''), value: '' }, { label: __(''), value: '' },
{ label: __('Twilio'), value: 'Twilio' }, { label: __('Twilio'), value: 'Twilio' },
{ label: __('Exotel'), value: 'Exotel' }, { label: __('Exotel'), value: 'Exotel' },
]" ]"
class="w-1/2" class="w-1/2"
:description="__('Default calling medium for logged in user')"
/> />
<!-- Twilio --> <!-- Twilio -->
<div class="flex flex-col justify-between gap-4"> <div v-if="isManager()" class="flex flex-col justify-between gap-4">
<span class="text-base font-semibold text-ink-gray-9"> <span class="text-base font-semibold text-ink-gray-9">
{{ __('Twilio') }} {{ __('Twilio') }}
</span> </span>
@ -37,12 +38,12 @@
v-if="twilio?.doc && twilioTabs" v-if="twilio?.doc && twilioTabs"
:tabs="twilioTabs" :tabs="twilioTabs"
:data="twilio.doc" :data="twilio.doc"
doctype="Twilio Settings" doctype="CRM Twilio Settings"
/> />
</div> </div>
<!-- Exotel --> <!-- Exotel -->
<div class="flex flex-col justify-between gap-4"> <div v-if="isManager()" class="flex flex-col justify-between gap-4">
<span class="text-base font-semibold text-ink-gray-9"> <span class="text-base font-semibold text-ink-gray-9">
{{ __('Exotel') }} {{ __('Exotel') }}
</span> </span>
@ -85,14 +86,17 @@ import {
call, call,
} from 'frappe-ui' } from 'frappe-ui'
import { defaultCallingMedium } from '@/composables/settings' import { defaultCallingMedium } from '@/composables/settings'
import { usersStore } from '@/stores/users'
import { createToast, getRandom } from '@/utils' import { createToast, getRandom } from '@/utils'
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
const { isManager, isAgent } = usersStore()
const twilioFields = createResource({ const twilioFields = createResource({
url: 'crm.api.doc.get_fields', url: 'crm.api.doc.get_fields',
cache: ['fields', 'Twilio Settings'], cache: ['fields', 'CRM Twilio Settings'],
params: { params: {
doctype: 'Twilio Settings', doctype: 'CRM Twilio Settings',
allow_all_fieldtypes: true, allow_all_fieldtypes: true,
}, },
auto: true, auto: true,
@ -109,8 +113,8 @@ const exotelFields = createResource({
}) })
const twilio = createDocumentResource({ const twilio = createDocumentResource({
doctype: 'Twilio Settings', doctype: 'CRM Twilio Settings',
name: 'Twilio Settings', name: 'CRM Twilio Settings',
fields: ['*'], fields: ['*'],
auto: true, auto: true,
setValue: { setValue: {
@ -273,6 +277,9 @@ function update() {
if (mediumChanged.value) { if (mediumChanged.value) {
updateMedium() updateMedium()
} }
if (!isManager()) return
if (twilio.isDirty) { if (twilio.isDirty) {
twilio.save.submit() twilio.save.submit()
} }
@ -298,6 +305,8 @@ async function updateMedium() {
const error = ref('') const error = ref('')
function validateIfDefaultMediumIsEnabled() { function validateIfDefaultMediumIsEnabled() {
if (isAgent() && !isManager()) return true
if (defaultCallingMedium.value === 'Twilio' && !twilio.doc.enabled) { if (defaultCallingMedium.value === 'Twilio' && !twilio.doc.enabled) {
error.value = __('Twilio is not enabled') error.value = __('Twilio is not enabled')
return false return false

View File

@ -53,9 +53,14 @@ export const usersStore = defineStore('crm-users', () => {
return getUser(email).is_manager return getUser(email).is_manager
} }
function isAgent(email) {
return getUser(email).is_agent
}
return { return {
users, users,
getUser, getUser,
isManager, isManager,
isAgent,
} }
}) })