diff --git a/crm/api/session.py b/crm/api/session.py index 0bc64db9..7650d3c1 100644 --- a/crm/api/session.py +++ b/crm/api/session.py @@ -60,6 +60,27 @@ def get_contacts(): return contacts +@frappe.whitelist() +def get_lead_contacts(): + if frappe.session.user == "Guest": + frappe.throw("Authentication failed", exc=frappe.AuthenticationError) + + lead_contacts = frappe.get_all( + "CRM Lead", + fields=[ + "name", + "lead_name", + "mobile_no", + "phone", + "image", + "modified" + ], + order_by="lead_name asc", + distinct=True, + ) + + return lead_contacts + @frappe.whitelist() def get_organizations(): if frappe.session.user == "Guest": diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.json b/crm/fcrm/doctype/crm_deal/crm_deal.json index 197633e2..64595b8b 100644 --- a/crm/fcrm/doctype/crm_deal/crm_deal.json +++ b/crm/fcrm/doctype/crm_deal/crm_deal.json @@ -51,8 +51,7 @@ "fieldname": "organization", "fieldtype": "Link", "label": "Organization", - "options": "CRM Organization", - "reqd": 1 + "options": "CRM Organization" }, { "fieldname": "probability", @@ -270,7 +269,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-06 18:45:26.263974", + "modified": "2024-01-13 11:50:37.361190", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Deal", diff --git a/crm/twilio/api.py b/crm/twilio/api.py index 93d2a535..21dac21e 100644 --- a/crm/twilio/api.py +++ b/crm/twilio/api.py @@ -4,6 +4,7 @@ import json import frappe from frappe import _ from .twilio_handler import Twilio, IncomingCall, TwilioCallDetails +from .utils import parse_mobile_no @frappe.whitelist() def is_enabled(): @@ -69,7 +70,7 @@ def create_call_log(call_details: TwilioCallDetails): 'doctype': 'CRM Call Log', 'medium': 'Twilio' }) - + call_log.reference_docname, call_log.reference_doctype = get_lead_or_deal_from_number(call_log) call_log.flags.ignore_permissions = True call_log.save() frappe.db.commit() @@ -82,11 +83,10 @@ def update_call_log(call_sid, status=None): call_details = twilio.get_call_info(call_sid) call_log = frappe.get_doc("CRM Call Log", call_sid) - call_log.status = status or TwilioCallDetails.get_call_status(call_details.status) + call_log.status = TwilioCallDetails.get_call_status(status or call_details.status) call_log.duration = call_details.duration call_log.start_time = get_datetime_from_timestamp(call_details.start_time) call_log.end_time = get_datetime_from_timestamp(call_details.end_time) - call_log.reference_docname, call_log.reference_doctype = get_lead_or_deal_from_number(call_log) if call_log.note and call_log.reference_docname: frappe.db.set_value("CRM Note", call_log.note, "reference_doctype", call_log.reference_doctype) frappe.db.set_value("CRM Note", call_log.note, "reference_docname", call_log.reference_docname) @@ -105,6 +105,15 @@ def update_recording_info(**kwargs): except: frappe.log_error(title=_("Failed to capture Twilio recording")) +@frappe.whitelist(allow_guest=True) +def update_call_status_info(**kwargs): + try: + args = frappe._dict(kwargs) + parent_call_sid = args.ParentCallSid + update_call_log(parent_call_sid, status=args.CallStatus) + except: + frappe.log_error(title=_("Failed to update Twilio call status")) + @frappe.whitelist(allow_guest=True) def get_call_info(**kwargs): """This is a webhook called when the outgoing call status changes. @@ -149,17 +158,25 @@ def add_note_to_call_log(call_sid, note): def get_lead_or_deal_from_number(call): """Get lead/deal from the given number. """ + + def find_record(doctype, mobile_no): + mobile_no = parse_mobile_no(mobile_no) + data = frappe.db.sql( + """ + SELECT name, mobile_no + FROM `tab{doctype}` + WHERE CONCAT('+', REGEXP_REPLACE(mobile_no, '[^0-9]', '')) = {mobile_no} + """.format(doctype=doctype, mobile_no=mobile_no), + as_dict=True + ) + return data[0].name if data else None + doctype = "CRM Lead" - doc = None - if call.type == 'Outgoing': - doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('to') }) - if not doc: - doctype = "CRM Deal" - doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('to') }) - else: - doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('from') }) - if not doc: - doctype = "CRM Deal" - doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('from') }) + number = call.get('to') if call.type == 'Outgoing' else call.get('from') + + doc = find_record(doctype, number) or None + if not doc: + doctype = "CRM Deal" + doc = find_record(doctype, number) return doc, doctype \ No newline at end of file diff --git a/crm/twilio/twilio_handler.py b/crm/twilio/twilio_handler.py index 71a64461..7c598818 100644 --- a/crm/twilio/twilio_handler.py +++ b/crm/twilio/twilio_handler.py @@ -72,8 +72,8 @@ class Twilio: url_path = "/api/method/crm.twilio.api.update_recording_info" return get_public_url(url_path) - def get_call_status_callback_url(self): - url_path = "/api/method/crm.twilio.api.get_call_info" + def get_update_call_status_callback_url(self): + url_path = "/api/method/crm.twilio.api.update_call_status_info" return get_public_url(url_path) def generate_twilio_dial_response(self, from_number: str, to_number: str): @@ -89,15 +89,12 @@ class Twilio: dial.number( to_number, status_callback_event='initiated ringing answered completed', - status_callback=self.get_call_status_callback_url(), + status_callback=self.get_update_call_status_callback_url(), status_callback_method='POST' ) resp.append(dial) return resp - def get_call_info(self, call_sid): - return self.twilio_client.calls(call_sid).fetch() - def generate_twilio_client_response(self, client, ring_tone='at'): """Generates voice call instructions to forward the call to agents computer. """ @@ -108,7 +105,12 @@ class Twilio: recording_status_callback=self.get_recording_status_callback_url(), recording_status_callback_event='completed' ) - dial.client(client) + dial.client( + client, + status_callback_event='initiated ringing answered completed', + status_callback=self.get_update_call_status_callback_url(), + status_callback_method='POST' + ) resp.append(dial) return resp @@ -156,6 +158,9 @@ def get_twilio_number_owners(phone_number): 'owner2': {....} } """ + # remove special characters from phone number and get only digits also remove white spaces + # keep + sign in the number at start of the number + phone_number = ''.join([c for c in phone_number if c.isdigit() or c == '+']) user_voice_settings = frappe.get_all( 'Twilio Agents', filters={'twilio_number': phone_number}, @@ -200,8 +205,8 @@ class TwilioCallDetails: self.application_sid = call_info.get('ApplicationSid') self.call_sid = call_info.get('CallSid') self.call_status = self.get_call_status(call_info.get('CallStatus')) - self._call_from = call_from - self._call_to = call_to + self._call_from = call_from or call_info.get('From') + self._call_to = call_to or call_info.get('To') def get_direction(self): if self.call_info.get('Caller').lower().startswith('client'): diff --git a/crm/twilio/utils.py b/crm/twilio/utils.py index 664b1083..2f037684 100644 --- a/crm/twilio/utils.py +++ b/crm/twilio/utils.py @@ -13,4 +13,11 @@ def merge_dicts(d1: dict, d2: dict): ) ... {'name1': {'age': 20, 'phone': '+xxx'}, 'name2': {'age': 30, 'phone': '+yyy'}} """ - return {k:{**v, **d2.get(k, {})} for k, v in d1.items()} \ No newline at end of file + return {k:{**v, **d2.get(k, {})} for k, v in d1.items()} + +def parse_mobile_no(mobile_no: str): + """Parse mobile number to remove spaces, brackets, etc. + >>> parse_mobile_no('+91 (766) 667 6666') + ... '+917666676666' + """ + return ''.join([c for c in mobile_no if c.isdigit() or c == '+']) \ No newline at end of file diff --git a/frappe-ui b/frappe-ui index f5f5665e..d2de676b 160000 --- a/frappe-ui +++ b/frappe-ui @@ -1 +1 @@ -Subproject commit f5f5665e944bef20a79ece6ad7223db09984c6a6 +Subproject commit d2de676b1e2eb4c5af36d22c006c1f4843c3e19b diff --git a/frontend/package.json b/frontend/package.json index 2abf1ef6..58e89d60 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,7 @@ "@vueuse/core": "^10.3.0", "@vueuse/integrations": "^10.3.0", "feather-icons": "^4.28.0", - "frappe-ui": "^0.1.22", + "frappe-ui": "^0.1.23", "mime": "^4.0.1", "pinia": "^2.0.33", "socket.io-client": "^4.7.2", diff --git a/frontend/src/components/Activities.vue b/frontend/src/components/Activities.vue index 02b97c02..2f9d0905 100644 --- a/frontend/src/components/Activities.vue +++ b/frontend/src/components/Activities.vue @@ -709,6 +709,7 @@ import { startCase, taskStatusOptions, } from '@/utils' +import { globalStore } from '@/stores/global' import { usersStore } from '@/stores/users' import { contactsStore } from '@/stores/contacts' import { @@ -724,8 +725,9 @@ import { import { useElementVisibility } from '@vueuse/core' import { ref, computed, h, defineModel, markRaw, watch, nextTick } from 'vue' +const { makeCall } = globalStore() const { getUser } = usersStore() -const { getContact } = contactsStore() +const { getContact, getLeadContact } = contactsStore() const props = defineProps({ title: { @@ -781,8 +783,11 @@ const calls = createListResource({ doc.duration = secondsToDuration(doc.duration) if (doc.type === 'Incoming') { doc.caller = { - label: getContact(doc.from)?.full_name || 'Unknown', - image: getContact(doc.from)?.image, + label: + getContact(doc.from)?.full_name || + getLeadContact(doc.from)?.full_name || + 'Unknown', + image: getContact(doc.from)?.image || getLeadContact(doc.from)?.image, } doc.receiver = { label: getUser(doc.receiver).full_name, @@ -794,8 +799,11 @@ const calls = createListResource({ image: getUser(doc.caller).user_image, } doc.receiver = { - label: getContact(doc.to)?.full_name || 'Unknown', - image: getContact(doc.to)?.image, + label: + getContact(doc.to)?.full_name || + getLeadContact(doc.to)?.full_name || + 'Unknown', + image: getContact(doc.to)?.image || getLeadContact(doc.to)?.image, } } }) diff --git a/frontend/src/components/CallUI.vue b/frontend/src/components/CallUI.vue index 2de01dff..3b999a20 100644 --- a/frontend/src/components/CallUI.vue +++ b/frontend/src/components/CallUI.vue @@ -183,11 +183,13 @@ import CountUpTimer from '@/components/CountUpTimer.vue' import NoteModal from '@/components/Modals/NoteModal.vue' import { Device } from '@twilio/voice-sdk' import { useDraggable, useWindowSize } from '@vueuse/core' +import { globalStore } from '@/stores/global' import { contactsStore } from '@/stores/contacts' import { Avatar, call } from 'frappe-ui' -import { onMounted, ref, watch, getCurrentInstance } from 'vue' +import { onMounted, ref, watch } from 'vue' -const { getContact } = contactsStore() +const { getContact, getLeadContact } = contactsStore() +const { setMakeCall, setTwilioEnabled } = globalStore() let device = '' let log = ref('Connecting...') @@ -197,7 +199,6 @@ const contact = ref({ mobile_no: '', }) -let enabled = ref(false) let showCallPopup = ref(false) let showSmallCallWindow = ref(false) let onCall = ref(false) @@ -308,8 +309,10 @@ function handleIncomingCall(call) { log.value = `Incoming call from ${call.parameters.From}` // get name of the caller from the phone number - contact.value = getContact(call.parameters.From) + if (!contact.value) { + contact.value = getLeadContact(call.parameters.From) + } if (!contact.value) { contact.value = { @@ -380,6 +383,9 @@ function handleDisconnectedIncomingCall() { async function makeOutgoingCall(number) { contact.value = getContact(number) + if (!contact.value) { + contact.value = getLeadContact(number) + } if (device) { log.value = `Attempting to call ${contact.value.mobile_no} ...` @@ -475,8 +481,11 @@ function toggleCallWindow() { } onMounted(async () => { - enabled.value = await is_twilio_enabled() - enabled.value && startupClient() + let enabled = await is_twilio_enabled() + setTwilioEnabled(enabled) + enabled && startupClient() + + setMakeCall(makeOutgoingCall) }) watch( @@ -486,10 +495,6 @@ watch( }, { immediate: true } ) - -const app = getCurrentInstance() -app.appContext.config.globalProperties.makeCall = makeOutgoingCall -app.appContext.config.globalProperties.is_twilio_enabled = enabled.value