diff --git a/crm/api/session.py b/crm/api/session.py index 2e2b93758..746de854e 100644 --- a/crm/api/session.py +++ b/crm/api/session.py @@ -5,7 +5,16 @@ def get_users(): users = frappe.qb.get_query( "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", distinct=True, ).run(as_dict=1) @@ -14,11 +23,13 @@ def get_users(): if frappe.session.user == user.name: user.session_user = True - user.is_manager = ( - "Sales Manager" in frappe.get_roles(user.name) or user.name == "Administrator" - ) + user.is_manager = "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 + @frappe.whitelist() def get_contacts(): contacts = frappe.get_all( @@ -37,7 +48,7 @@ def get_contacts(): "mobile_no", "phone", "company_name", - "modified" + "modified", ], order_by="first_name asc", distinct=True, @@ -58,18 +69,12 @@ def get_contacts(): return contacts + @frappe.whitelist() def get_lead_contacts(): lead_contacts = frappe.get_all( "CRM Lead", - fields=[ - "name", - "lead_name", - "mobile_no", - "phone", - "image", - "modified" - ], + fields=["name", "lead_name", "mobile_no", "phone", "image", "modified"], filters={"converted": 0}, order_by="lead_name asc", distinct=True, @@ -77,11 +82,12 @@ def get_lead_contacts(): return lead_contacts + @frappe.whitelist() def get_organizations(): organizations = frappe.qb.get_query( "CRM Organization", - fields=['*'], + fields=["*"], order_by="name asc", distinct=True, ).run(as_dict=1) diff --git a/crm/fcrm/doctype/crm_exotel_agent/__init__.py b/crm/fcrm/doctype/crm_telephony_agent/__init__.py similarity index 100% rename from crm/fcrm/doctype/crm_exotel_agent/__init__.py rename to crm/fcrm/doctype/crm_telephony_agent/__init__.py diff --git a/crm/fcrm/doctype/crm_exotel_agent/crm_exotel_agent.js b/crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.js similarity index 77% rename from crm/fcrm/doctype/crm_exotel_agent/crm_exotel_agent.js rename to crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.js index 78f3c9bbf..b1bea1732 100644 --- a/crm/fcrm/doctype/crm_exotel_agent/crm_exotel_agent.js +++ b/crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.js @@ -1,7 +1,7 @@ // Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -// frappe.ui.form.on("CRM Exotel Agent", { +// frappe.ui.form.on("CRM Telephony Agent", { // refresh(frm) { // }, diff --git a/crm/fcrm/doctype/crm_exotel_agent/crm_exotel_agent.json b/crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json similarity index 51% rename from crm/fcrm/doctype/crm_exotel_agent/crm_exotel_agent.json rename to crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json index c9baa7851..042632f69 100644 --- a/crm/fcrm/doctype/crm_exotel_agent/crm_exotel_agent.json +++ b/crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json @@ -1,7 +1,7 @@ { "actions": [], "allow_rename": 1, - "autoname": "field:mobile_no", + "autoname": "field:user", "creation": "2025-01-11 16:12:46.602782", "doctype": "DocType", "engine": "InnoDB", @@ -10,7 +10,15 @@ "user_name", "column_break_hdec", "mobile_no", - "exotel_number" + "default_medium", + "section_break_ozjn", + "twilio", + "twilio_number", + "column_break_aydj", + "exotel", + "exotel_number", + "section_break_phlq", + "phone_nos" ], "fields": [ { @@ -20,7 +28,8 @@ "in_standard_filter": 1, "label": "User", "options": "User", - "reqd": 1 + "reqd": 1, + "unique": 1 }, { "fieldname": "column_break_hdec", @@ -32,8 +41,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Mobile No.", - "reqd": 1, - "unique": 1 + "read_only": 1 }, { "fetch_from": "user.full_name", @@ -44,19 +52,62 @@ "label": "User Name" }, { + "depends_on": "exotel", "fieldname": "exotel_number", "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Exotel Number" + "label": "Exotel Number", + "mandatory_depends_on": "exotel" + }, + { + "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, "links": [], - "modified": "2025-01-15 20:03:31.162162", + "modified": "2025-01-19 14:17:12.880185", "modified_by": "Administrator", "module": "FCRM", - "name": "CRM Exotel Agent", + "name": "CRM Telephony Agent", "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ diff --git a/crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.py b/crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.py new file mode 100644 index 000000000..ff1f85e9b --- /dev/null +++ b/crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.py @@ -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 = "" diff --git a/crm/fcrm/doctype/crm_exotel_agent/test_crm_exotel_agent.py b/crm/fcrm/doctype/crm_telephony_agent/test_crm_telephony_agent.py similarity index 77% rename from crm/fcrm/doctype/crm_exotel_agent/test_crm_exotel_agent.py rename to crm/fcrm/doctype/crm_telephony_agent/test_crm_telephony_agent.py index 9fe610ce7..63e0f2fb7 100644 --- a/crm/fcrm/doctype/crm_exotel_agent/test_crm_exotel_agent.py +++ b/crm/fcrm/doctype/crm_telephony_agent/test_crm_telephony_agent.py @@ -4,7 +4,6 @@ # import frappe from frappe.tests import IntegrationTestCase, UnitTestCase - # On IntegrationTestCase, the doctype test records and all # link-field test record dependencies are recursively loaded # Use these module variables to add/remove to/from that list @@ -12,18 +11,18 @@ 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. """ pass -class IntegrationTestCRMExotelAgent(IntegrationTestCase): +class IntegrationTestCRMTelephonyAgent(IntegrationTestCase): """ - Integration tests for CRMExotelAgent. + Integration tests for CRMTelephonyAgent. Use this class for testing interactions between multiple components. """ diff --git a/crm/fcrm/doctype/twilio_agents/__init__.py b/crm/fcrm/doctype/crm_telephony_phone/__init__.py similarity index 100% rename from crm/fcrm/doctype/twilio_agents/__init__.py rename to crm/fcrm/doctype/crm_telephony_phone/__init__.py diff --git a/crm/fcrm/doctype/crm_telephony_phone/crm_telephony_phone.json b/crm/fcrm/doctype/crm_telephony_phone/crm_telephony_phone.json new file mode 100644 index 000000000..6450a96a8 --- /dev/null +++ b/crm/fcrm/doctype/crm_telephony_phone/crm_telephony_phone.json @@ -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": [] +} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_exotel_agent/crm_exotel_agent.py b/crm/fcrm/doctype/crm_telephony_phone/crm_telephony_phone.py similarity index 84% rename from crm/fcrm/doctype/crm_exotel_agent/crm_exotel_agent.py rename to crm/fcrm/doctype/crm_telephony_phone/crm_telephony_phone.py index 05fa7ee74..5522b84db 100644 --- a/crm/fcrm/doctype/crm_exotel_agent/crm_exotel_agent.py +++ b/crm/fcrm/doctype/crm_telephony_phone/crm_telephony_phone.py @@ -5,5 +5,5 @@ from frappe.model.document import Document -class CRMExotelAgent(Document): +class CRMTelephonyPhone(Document): pass diff --git a/crm/fcrm/doctype/twilio_settings/__init__.py b/crm/fcrm/doctype/crm_twilio_settings/__init__.py similarity index 100% rename from crm/fcrm/doctype/twilio_settings/__init__.py rename to crm/fcrm/doctype/crm_twilio_settings/__init__.py diff --git a/crm/fcrm/doctype/twilio_settings/twilio_settings.js b/crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.js similarity index 77% rename from crm/fcrm/doctype/twilio_settings/twilio_settings.js rename to crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.js index 9837c18d4..1bfa5e446 100644 --- a/crm/fcrm/doctype/twilio_settings/twilio_settings.js +++ b/crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.js @@ -1,7 +1,7 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -// frappe.ui.form.on("Twilio Settings", { +// frappe.ui.form.on("CRM Twilio Settings", { // refresh(frm) { // }, diff --git a/crm/fcrm/doctype/twilio_settings/twilio_settings.json b/crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.json similarity index 98% rename from crm/fcrm/doctype/twilio_settings/twilio_settings.json rename to crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.json index 17e92cfbd..d898b631e 100644 --- a/crm/fcrm/doctype/twilio_settings/twilio_settings.json +++ b/crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.json @@ -108,7 +108,7 @@ "modified": "2025-01-15 19:35:13.406254", "modified_by": "Administrator", "module": "FCRM", - "name": "Twilio Settings", + "name": "CRM Twilio Settings", "owner": "Administrator", "permissions": [ { diff --git a/crm/fcrm/doctype/twilio_settings/twilio_settings.py b/crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.py similarity index 76% rename from crm/fcrm/doctype/twilio_settings/twilio_settings.py rename to crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.py index 1d3a20b63..f2737c6ce 100644 --- a/crm/fcrm/doctype/twilio_settings/twilio_settings.py +++ b/crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.py @@ -2,13 +2,13 @@ # For license information, please see license.txt import frappe -from frappe.model.document import Document from frappe import _ - +from frappe.model.document import Document 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): self.validate_twilio_account() @@ -33,28 +33,26 @@ def validate_twilio_account(self): frappe.throw(_("Invalid Account SID or Auth Token.")) 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: 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 - }) + frappe.db.set_value( + "CRM Twilio Settings", + "CRM 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. - """ + """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) + frappe.db.set_value("CRM Twilio Settings", "CRM Twilio Settings", "twiml_sid", self.twiml_sid) def create_api_key(self, twilio): - """Create API keys in twilio account. - """ + """Create API keys in twilio account.""" try: return twilio.new_keys.create(friendly_name=self.friendly_resource_name) except Exception: @@ -66,23 +64,21 @@ def get_twilio_voice_url(self): return get_public_url(url_path) 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 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. - """ + """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 - ) + voice_method="POST", voice_url=self.get_twilio_voice_url(), friendly_name=friendly_name + ) return application -def get_public_url(path: str=None): + +def get_public_url(path: str | None = None): from frappe.utils import get_url - return get_url().split(":8", 1)[0] + path \ No newline at end of file + + return get_url().split(":8", 1)[0] + path diff --git a/crm/fcrm/doctype/twilio_agents/test_twilio_agents.py b/crm/fcrm/doctype/crm_twilio_settings/test_crm_twilio_settings.py similarity index 77% rename from crm/fcrm/doctype/twilio_agents/test_twilio_agents.py rename to crm/fcrm/doctype/crm_twilio_settings/test_crm_twilio_settings.py index 29ecc305a..531076afc 100644 --- a/crm/fcrm/doctype/twilio_agents/test_twilio_agents.py +++ b/crm/fcrm/doctype/crm_twilio_settings/test_crm_twilio_settings.py @@ -5,5 +5,5 @@ from frappe.tests import UnitTestCase -class TestTwilioAgents(UnitTestCase): +class TestCRMTwilioSettings(UnitTestCase): pass diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json index 0a902c992..f445541d1 100644 --- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json +++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json @@ -12,9 +12,7 @@ "brand_logo", "favicon", "dropdown_items_tab", - "dropdown_items", - "calling_tab", - "default_calling_medium" + "dropdown_items" ], "fields": [ { @@ -58,23 +56,12 @@ "fieldname": "favicon", "fieldtype": "Attach", "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, "issingle": 1, "links": [], - "modified": "2025-01-15 17:40:32.784762", + "modified": "2025-01-19 14:23:05.981355", "modified_by": "Administrator", "module": "FCRM", "name": "FCRM Settings", diff --git a/crm/fcrm/doctype/twilio_agents/twilio_agents.js b/crm/fcrm/doctype/twilio_agents/twilio_agents.js deleted file mode 100644 index 8d8bf2944..000000000 --- a/crm/fcrm/doctype/twilio_agents/twilio_agents.js +++ /dev/null @@ -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) { - -// }, -// }); diff --git a/crm/fcrm/doctype/twilio_agents/twilio_agents.json b/crm/fcrm/doctype/twilio_agents/twilio_agents.json deleted file mode 100644 index 652511304..000000000 --- a/crm/fcrm/doctype/twilio_agents/twilio_agents.json +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/crm/fcrm/doctype/twilio_agents/twilio_agents.py b/crm/fcrm/doctype/twilio_agents/twilio_agents.py deleted file mode 100644 index fb660dd87..000000000 --- a/crm/fcrm/doctype/twilio_agents/twilio_agents.py +++ /dev/null @@ -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 diff --git a/crm/fcrm/doctype/twilio_settings/test_twilio_settings.py b/crm/fcrm/doctype/twilio_settings/test_twilio_settings.py deleted file mode 100644 index 21b038410..000000000 --- a/crm/fcrm/doctype/twilio_settings/test_twilio_settings.py +++ /dev/null @@ -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 diff --git a/crm/integrations/api.py b/crm/integrations/api.py index 145bf03a0..7bb15f898 100644 --- a/crm/integrations/api.py +++ b/crm/integrations/api.py @@ -7,20 +7,42 @@ @frappe.whitelist() 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") - default_calling_medium = frappe.db.get_single_value("FCRM Settings", "default_calling_medium") return { "twilio_enabled": twilio_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() 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() diff --git a/crm/integrations/exotel/handler.py b/crm/integrations/exotel/handler.py index 1f01837c7..7d3301a8c 100644 --- a/crm/integrations/exotel/handler.py +++ b/crm/integrations/exotel/handler.py @@ -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") 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: - 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(): frappe.throw(_("Exotel Number {0} is not valid").format(caller_id), title=_("Invalid Exotel Number")) if not from_number: 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") diff --git a/crm/integrations/twilio/api.py b/crm/integrations/twilio/api.py index e78e0214b..0df049ae6 100644 --- a/crm/integrations/twilio/api.py +++ b/crm/integrations/twilio/api.py @@ -11,7 +11,7 @@ @frappe.whitelist() def is_enabled(): - return frappe.db.get_single_value("Twilio Settings", "enabled") + return frappe.db.get_single_value("CRM Twilio Settings", "enabled") @frappe.whitelist() diff --git a/crm/integrations/twilio/twilio_handler.py b/crm/integrations/twilio/twilio_handler.py index b24e0e9d3..2d6b7df1f 100644 --- a/crm/integrations/twilio/twilio_handler.py +++ b/crm/integrations/twilio/twilio_handler.py @@ -14,7 +14,7 @@ class Twilio: def __init__(self, settings): """ - :param settings: `Twilio Settings` doctype + :param settings: `CRM Twilio Settings` doctype """ self.settings = settings self.account_sid = settings.account_sid @@ -26,7 +26,7 @@ def __init__(self, settings): @classmethod def connect(self): """Make a twilio connection.""" - settings = frappe.get_doc("Twilio Settings") + settings = frappe.get_doc("CRM Twilio Settings") if not (settings and settings.enabled): return return Twilio(settings=settings) @@ -114,11 +114,11 @@ def generate_twilio_client_response(self, client, ring_tone="at"): @classmethod 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: 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) return client diff --git a/crm/patches.txt b/crm/patches.txt index f964d13fb..f8fbb4c6d 100644 --- a/crm/patches.txt +++ b/crm/patches.txt @@ -2,6 +2,7 @@ # 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 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] # 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_sidebar_fields_layout crm.patches.v1_0.update_deal_quick_entry_layout -crm.patches.v1_0.update_layouts_to_new_format \ No newline at end of file +crm.patches.v1_0.update_layouts_to_new_format +crm.patches.v1_0.move_twilio_agent_to_telephony_agent \ No newline at end of file diff --git a/crm/patches/v1_0/move_twilio_agent_to_telephony_agent.py b/crm/patches/v1_0/move_twilio_agent_to_telephony_agent.py new file mode 100644 index 000000000..7049ab628 --- /dev/null +++ b/crm/patches/v1_0/move_twilio_agent_to_telephony_agent.py @@ -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() diff --git a/crm/patches/v1_0/rename_twilio_settings_to_crm_twilio_settings.py b/crm/patches/v1_0/rename_twilio_settings_to_crm_twilio_settings.py new file mode 100644 index 000000000..349764843 --- /dev/null +++ b/crm/patches/v1_0/rename_twilio_settings_to_crm_twilio_settings.py @@ -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() diff --git a/frontend/src/components/Settings/Settings.vue b/frontend/src/components/Settings/Settings.vue index 2f1c981db..fde939388 100644 --- a/frontend/src/components/Settings/Settings.vue +++ b/frontend/src/components/Settings/Settings.vue @@ -67,7 +67,7 @@ import { import { Dialog, Button, Avatar } from 'frappe-ui' import { ref, markRaw, computed, watch, h } from 'vue' -const { isManager, getUser } = usersStore() +const { isManager, isAgent, getUser } = usersStore() const user = computed(() => getUser() || {}) @@ -108,20 +108,22 @@ const tabs = computed(() => { label: __('Telephony'), icon: PhoneIcon, component: markRaw(TelephonySettings), + condition: () => isManager() || isAgent(), }, { label: __('WhatsApp'), icon: WhatsAppIcon, component: markRaw(WhatsAppSettings), - condition: () => isWhatsappInstalled.value, + condition: () => isWhatsappInstalled.value && isManager(), }, { label: __('ERPNext'), icon: ERPNextIcon, component: markRaw(ERPNextSettings), + condition: () => isManager(), }, ], - condition: () => isManager(), + condition: () => isManager() || isAgent(), }, ] diff --git a/frontend/src/components/Settings/TelephonySettings.vue b/frontend/src/components/Settings/TelephonySettings.vue index 8134df82d..b7df8c351 100644 --- a/frontend/src/components/Settings/TelephonySettings.vue +++ b/frontend/src/components/Settings/TelephonySettings.vue @@ -19,17 +19,18 @@ -
+
{{ __('Twilio') }} @@ -37,12 +38,12 @@ v-if="twilio?.doc && twilioTabs" :tabs="twilioTabs" :data="twilio.doc" - doctype="Twilio Settings" + doctype="CRM Twilio Settings" />
-
+
{{ __('Exotel') }} @@ -85,14 +86,17 @@ import { call, } from 'frappe-ui' import { defaultCallingMedium } from '@/composables/settings' +import { usersStore } from '@/stores/users' import { createToast, getRandom } from '@/utils' import { ref, computed, watch } from 'vue' +const { isManager, isAgent } = usersStore() + const twilioFields = createResource({ url: 'crm.api.doc.get_fields', - cache: ['fields', 'Twilio Settings'], + cache: ['fields', 'CRM Twilio Settings'], params: { - doctype: 'Twilio Settings', + doctype: 'CRM Twilio Settings', allow_all_fieldtypes: true, }, auto: true, @@ -109,8 +113,8 @@ const exotelFields = createResource({ }) const twilio = createDocumentResource({ - doctype: 'Twilio Settings', - name: 'Twilio Settings', + doctype: 'CRM Twilio Settings', + name: 'CRM Twilio Settings', fields: ['*'], auto: true, setValue: { @@ -273,6 +277,9 @@ function update() { if (mediumChanged.value) { updateMedium() } + + if (!isManager()) return + if (twilio.isDirty) { twilio.save.submit() } @@ -298,6 +305,8 @@ async function updateMedium() { const error = ref('') function validateIfDefaultMediumIsEnabled() { + if (isAgent() && !isManager()) return true + if (defaultCallingMedium.value === 'Twilio' && !twilio.doc.enabled) { error.value = __('Twilio is not enabled') return false diff --git a/frontend/src/stores/users.js b/frontend/src/stores/users.js index bee56d562..5965c537e 100644 --- a/frontend/src/stores/users.js +++ b/frontend/src/stores/users.js @@ -53,9 +53,14 @@ export const usersStore = defineStore('crm-users', () => { return getUser(email).is_manager } + function isAgent(email) { + return getUser(email).is_agent + } + return { users, getUser, isManager, + isAgent, } })