From 8b0588deaff36bffe41be0aafebab0481a1bc6d3 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 2 May 2019 14:59:44 +0530 Subject: [PATCH 01/50] feat: Init call summary popup --- .../crm/call_summary/call_summary_utils.py | 10 ++++++++ erpnext/public/build.json | 3 ++- erpnext/public/js/call_summary_dialog.js | 24 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 erpnext/crm/call_summary/call_summary_utils.py create mode 100644 erpnext/public/js/call_summary_dialog.js diff --git a/erpnext/crm/call_summary/call_summary_utils.py b/erpnext/crm/call_summary/call_summary_utils.py new file mode 100644 index 0000000000..822fb3e038 --- /dev/null +++ b/erpnext/crm/call_summary/call_summary_utils.py @@ -0,0 +1,10 @@ +import frappe + +@frappe.whitelist() +def get_contact_doc(phone_number): + contacts = frappe.get_all('Contact', filters={ + 'phone': phone_number + }, fields=['*']) + + if contacts: + return contacts[0] \ No newline at end of file diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 45de6eb294..25fe0d61a3 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -48,7 +48,8 @@ "public/js/utils/customer_quick_entry.js", "public/js/education/student_button.html", "public/js/education/assessment_result_tool.html", - "public/js/hub/hub_factory.js" + "public/js/hub/hub_factory.js", + "public/js/call_summary_dialog.js" ], "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", diff --git a/erpnext/public/js/call_summary_dialog.js b/erpnext/public/js/call_summary_dialog.js new file mode 100644 index 0000000000..c4c6d483eb --- /dev/null +++ b/erpnext/public/js/call_summary_dialog.js @@ -0,0 +1,24 @@ +frappe.call_summary_dialog = class { + constructor(opts) { + this.number = '+91234444444'; + this.make(); + } + + make() { + var d = new frappe.ui.Dialog(); + this.$modal_body = $(d.body); + this.call_summary_dialog = d; + $(d.header).html(`
Incoming Call: ${this.number}
`); + frappe.xcall('erpnext.crm.call_summary.call_summary_utils.get_contact_doc', { + phone_number: this.number + }).then(res => { + if (!res) { + this.$modal_body.html('Unknown Contact'); + } else { + this.$modal_body.html(`${res.first_name}`); + } + }); + d.show(); + } + +}; \ No newline at end of file From 03c3bd5f4a2f61361a0ae6511ae01e5a01cdf7bf Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 16 May 2019 13:39:50 +0530 Subject: [PATCH 02/50] feat: Open modal in realtime for incoming call --- erpnext/crm/call_summary/call_summary_utils.py | 6 ++++-- erpnext/crm/call_summary/exotel_call_handler.py | 12 ++++++++++++ erpnext/public/js/call_summary_dialog.js | 16 ++++++++++++---- 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 erpnext/crm/call_summary/exotel_call_handler.py diff --git a/erpnext/crm/call_summary/call_summary_utils.py b/erpnext/crm/call_summary/call_summary_utils.py index 822fb3e038..0b6131ffd8 100644 --- a/erpnext/crm/call_summary/call_summary_utils.py +++ b/erpnext/crm/call_summary/call_summary_utils.py @@ -2,8 +2,10 @@ import frappe @frappe.whitelist() def get_contact_doc(phone_number): - contacts = frappe.get_all('Contact', filters={ - 'phone': phone_number + phone_number = phone_number[-10:] + contacts = frappe.get_all('Contact', or_filters={ + 'phone': ['like', '%{}%'.format(phone_number)], + 'mobile_no': ['like', '%{}%'.format(phone_number)] }, fields=['*']) if contacts: diff --git a/erpnext/crm/call_summary/exotel_call_handler.py b/erpnext/crm/call_summary/exotel_call_handler.py new file mode 100644 index 0000000000..82c925de06 --- /dev/null +++ b/erpnext/crm/call_summary/exotel_call_handler.py @@ -0,0 +1,12 @@ +import frappe + +@frappe.whitelist(allow_guest=True) +def handle_request(*args, **kwargs): + r = frappe.request + + payload = r.get_data() + + print(r.args.to_dict()) + print(payload) + + frappe.publish_realtime('incoming_call', r.args.to_dict()) \ No newline at end of file diff --git a/erpnext/public/js/call_summary_dialog.js b/erpnext/public/js/call_summary_dialog.js index c4c6d483eb..17bf7b9766 100644 --- a/erpnext/public/js/call_summary_dialog.js +++ b/erpnext/public/js/call_summary_dialog.js @@ -1,6 +1,6 @@ -frappe.call_summary_dialog = class { +class CallSummaryDialog { constructor(opts) { - this.number = '+91234444444'; + this.number = opts.number; this.make(); } @@ -15,10 +15,18 @@ frappe.call_summary_dialog = class { if (!res) { this.$modal_body.html('Unknown Contact'); } else { - this.$modal_body.html(`${res.first_name}`); + this.$modal_body.append(`${frappe.utils.get_form_link('Contact', res.name, true)}`) } }); d.show(); } +} -}; \ No newline at end of file +$(document).on('app_ready', function() { + frappe.realtime.on('incoming_call', data => { + const number = data.CallFrom; + frappe.call_summary_dialog = new CallSummaryDialog({ + number + }); + }); +}); From dd6b70c7cdf0f323ee8b61df8795bdae942853a8 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 16 May 2019 23:55:35 +0530 Subject: [PATCH 03/50] feat: Show contact on incoming call --- .../crm/call_summary/call_summary_utils.py | 12 ++- erpnext/public/build.json | 6 +- .../js/call_popup/call_summary_dialog.js | 101 ++++++++++++++++++ erpnext/public/js/call_summary_dialog.js | 32 ------ erpnext/public/less/call_summary.less | 6 ++ 5 files changed, 120 insertions(+), 37 deletions(-) create mode 100644 erpnext/public/js/call_popup/call_summary_dialog.js delete mode 100644 erpnext/public/js/call_summary_dialog.js create mode 100644 erpnext/public/less/call_summary.less diff --git a/erpnext/crm/call_summary/call_summary_utils.py b/erpnext/crm/call_summary/call_summary_utils.py index 0b6131ffd8..5814e2b97b 100644 --- a/erpnext/crm/call_summary/call_summary_utils.py +++ b/erpnext/crm/call_summary/call_summary_utils.py @@ -4,9 +4,15 @@ import frappe def get_contact_doc(phone_number): phone_number = phone_number[-10:] contacts = frappe.get_all('Contact', or_filters={ - 'phone': ['like', '%{}%'.format(phone_number)], - 'mobile_no': ['like', '%{}%'.format(phone_number)] + 'phone': ['like', '%{}'.format(phone_number)], + 'mobile_no': ['like', '%{}'.format(phone_number)] }, fields=['*']) if contacts: - return contacts[0] \ No newline at end of file + return contacts[0] + +@frappe.whitelist() +def get_last_communication(phone_number, customer=None): + # find last communication through phone_number + # find last issues, opportunity, lead + pass \ No newline at end of file diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 25fe0d61a3..3f55d0737d 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -1,7 +1,8 @@ { "css/erpnext.css": [ "public/less/erpnext.less", - "public/less/hub.less" + "public/less/hub.less", + "public/less/call_summary.less" ], "css/marketplace.css": [ "public/less/hub.less" @@ -49,7 +50,8 @@ "public/js/education/student_button.html", "public/js/education/assessment_result_tool.html", "public/js/hub/hub_factory.js", - "public/js/call_summary_dialog.js" + "public/js/call_popup/call_summary_dialog.js", + "public/js/call_popup/call_summary.html" ], "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", diff --git a/erpnext/public/js/call_popup/call_summary_dialog.js b/erpnext/public/js/call_popup/call_summary_dialog.js new file mode 100644 index 0000000000..9909b709c9 --- /dev/null +++ b/erpnext/public/js/call_popup/call_summary_dialog.js @@ -0,0 +1,101 @@ +class CallSummaryDialog { + constructor(opts) { + this.number = opts.number; + this.make(); + } + + make() { + var d = new frappe.ui.Dialog({ + 'title': `Incoming Call: ${this.number}`, + 'fields': [{ + 'fieldname': 'customer_info', + 'fieldtype': 'HTML' + }, { + 'fieldtype': 'Section Break' + }, { + 'fieldtype': 'Text', + 'label': "Last Communication", + 'fieldname': 'last_communication', + 'default': 'This is not working please helpppp', + 'placeholder': __("Select or add new customer"), + 'readonly': true + }, { + 'fieldtype': 'Column Break' + }, { + 'fieldtype': 'Text', + 'label': 'Call Summary', + 'fieldname': 'call_communication', + 'default': 'This is not working please helpppp', + "placeholder": __("Select or add new customer") + }] + }); + // this.body.html(this.get_dialog_skeleton()); + frappe.xcall('erpnext.crm.call_summary.call_summary_utils.get_contact_doc', { + phone_number: this.number + }).then(res => { + this.make_customer_contact(res, d.fields_dict["customer_info"].$wrapper); + // this.make_last_communication_section(); + }); + d.show(); + } + + get_dialog_skeleton() { + return ` +
+
+
+
+
+
+
+
+
+
+
+
+
+ `; + } + make_customer_contact(res, wrapper) { + if (!res) { + wrapper.append('Unknown Contact'); + } else { + wrapper.append(` + +
+ ${res.first_name} ${res.last_name} + ${res.mobile_no} + Customer: Some Enterprise +
+ `); + } + } + + make_last_communication_section() { + const last_communication_section = this.body.find('.last-communication'); + const last_communication = frappe.ui.form.make_control({ + parent: last_communication_section, + df: { + fieldtype: "Text", + label: "Last Communication", + fieldname: "last_communication", + 'default': 'This is not working please helpppp', + "placeholder": __("Select or add new customer") + }, + }); + last_communication.set_value('This is not working please helpppp'); + } + + make_summary_section() { + // + } +} + +$(document).on('app_ready', function() { + frappe.realtime.on('incoming_call', data => { + const number = data.CallFrom; + frappe.call_summary_dialog = new CallSummaryDialog({ + number + }); + }); +}); diff --git a/erpnext/public/js/call_summary_dialog.js b/erpnext/public/js/call_summary_dialog.js deleted file mode 100644 index 17bf7b9766..0000000000 --- a/erpnext/public/js/call_summary_dialog.js +++ /dev/null @@ -1,32 +0,0 @@ -class CallSummaryDialog { - constructor(opts) { - this.number = opts.number; - this.make(); - } - - make() { - var d = new frappe.ui.Dialog(); - this.$modal_body = $(d.body); - this.call_summary_dialog = d; - $(d.header).html(`
Incoming Call: ${this.number}
`); - frappe.xcall('erpnext.crm.call_summary.call_summary_utils.get_contact_doc', { - phone_number: this.number - }).then(res => { - if (!res) { - this.$modal_body.html('Unknown Contact'); - } else { - this.$modal_body.append(`${frappe.utils.get_form_link('Contact', res.name, true)}`) - } - }); - d.show(); - } -} - -$(document).on('app_ready', function() { - frappe.realtime.on('incoming_call', data => { - const number = data.CallFrom; - frappe.call_summary_dialog = new CallSummaryDialog({ - number - }); - }); -}); diff --git a/erpnext/public/less/call_summary.less b/erpnext/public/less/call_summary.less new file mode 100644 index 0000000000..73f6fd4f77 --- /dev/null +++ b/erpnext/public/less/call_summary.less @@ -0,0 +1,6 @@ +.customer-info { + img { + width: auto; + height: 100px; + } +} \ No newline at end of file From 8a178d6f300e066a9187500f73ed058203fc7806 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Sat, 18 May 2019 16:11:29 +0530 Subject: [PATCH 04/50] fix: Update call summary dialog --- .../crm/call_summary/call_summary_utils.py | 4 +- .../crm/call_summary/exotel_call_handler.py | 41 ++++++++++-- .../js/call_popup/call_summary_dialog.js | 66 +++++++++++-------- erpnext/public/less/call_summary.less | 1 + 4 files changed, 76 insertions(+), 36 deletions(-) diff --git a/erpnext/crm/call_summary/call_summary_utils.py b/erpnext/crm/call_summary/call_summary_utils.py index 5814e2b97b..49491d5a4a 100644 --- a/erpnext/crm/call_summary/call_summary_utils.py +++ b/erpnext/crm/call_summary/call_summary_utils.py @@ -6,10 +6,10 @@ def get_contact_doc(phone_number): contacts = frappe.get_all('Contact', or_filters={ 'phone': ['like', '%{}'.format(phone_number)], 'mobile_no': ['like', '%{}'.format(phone_number)] - }, fields=['*']) + }, fields=['name']) if contacts: - return contacts[0] + return frappe.get_doc(contacts[0].name) @frappe.whitelist() def get_last_communication(phone_number, customer=None): diff --git a/erpnext/crm/call_summary/exotel_call_handler.py b/erpnext/crm/call_summary/exotel_call_handler.py index 82c925de06..a0c3b7d5ea 100644 --- a/erpnext/crm/call_summary/exotel_call_handler.py +++ b/erpnext/crm/call_summary/exotel_call_handler.py @@ -2,11 +2,42 @@ import frappe @frappe.whitelist(allow_guest=True) def handle_request(*args, **kwargs): - r = frappe.request + # r = frappe.request - payload = r.get_data() + # print(r.args.to_dict(), args, kwargs) - print(r.args.to_dict()) - print(payload) + incoming_phone_number = kwargs.get('CallFrom') + contact = get_contact_doc(incoming_phone_number) + last_communication = get_last_communication(incoming_phone_number, contact) - frappe.publish_realtime('incoming_call', r.args.to_dict()) \ No newline at end of file + data = { + 'contact': contact, + 'call_payload': kwargs, + 'last_communication': last_communication + } + + frappe.publish_realtime('incoming_call', data) + + +def get_contact_doc(phone_number): + phone_number = phone_number[-10:] + number_filter = { + 'phone': ['like', '%{}'.format(phone_number)], + 'mobile_no': ['like', '%{}'.format(phone_number)] + } + contacts = frappe.get_all('Contact', or_filters=number_filter, + fields=['name'], limit=1) + + if contacts: + return frappe.get_doc('Contact', contacts[0].name) + + leads = frappe.get_all('Leads', or_filters=number_filter, + fields=['name'], limit=1) + + if leads: + return frappe.get_doc('Lead', leads[0].name) + + +def get_last_communication(phone_number, contact): + # frappe.get_all('Communication', filter={}) + return {} diff --git a/erpnext/public/js/call_popup/call_summary_dialog.js b/erpnext/public/js/call_popup/call_summary_dialog.js index 9909b709c9..e9823eeeef 100644 --- a/erpnext/public/js/call_popup/call_summary_dialog.js +++ b/erpnext/public/js/call_popup/call_summary_dialog.js @@ -1,42 +1,46 @@ class CallSummaryDialog { - constructor(opts) { - this.number = opts.number; + constructor({ contact, call_payload, last_communication }) { + this.number = call_payload.CallFrom; + this.contact = contact; + this.last_communication = last_communication; this.make(); } make() { - var d = new frappe.ui.Dialog({ - 'title': `Incoming Call: ${this.number}`, + this.dialog = new frappe.ui.Dialog({ + 'title': __(`Incoming call from ${this.contact ? this.contact.name : 'Unknown Number'}`), + 'static': true, + 'minimizable': true, 'fields': [{ 'fieldname': 'customer_info', 'fieldtype': 'HTML' }, { 'fieldtype': 'Section Break' }, { - 'fieldtype': 'Text', + 'fieldtype': 'Small Text', 'label': "Last Communication", 'fieldname': 'last_communication', - 'default': 'This is not working please helpppp', - 'placeholder': __("Select or add new customer"), - 'readonly': true + 'read_only': true }, { 'fieldtype': 'Column Break' }, { - 'fieldtype': 'Text', + 'fieldtype': 'Small Text', 'label': 'Call Summary', 'fieldname': 'call_communication', 'default': 'This is not working please helpppp', "placeholder": __("Select or add new customer") + }, { + 'fieldtype': 'Button', + 'label': 'Submit', + 'click': () => { + frappe.xcall() + } }] }); - // this.body.html(this.get_dialog_skeleton()); - frappe.xcall('erpnext.crm.call_summary.call_summary_utils.get_contact_doc', { - phone_number: this.number - }).then(res => { - this.make_customer_contact(res, d.fields_dict["customer_info"].$wrapper); - // this.make_last_communication_section(); - }); - d.show(); + this.make_customer_contact(); + this.dialog.show(); + this.dialog.get_close_btn().show(); + this.dialog.header.find('.indicator').removeClass('hidden').addClass('blue'); } get_dialog_skeleton() { @@ -56,16 +60,23 @@ class CallSummaryDialog { `; } - make_customer_contact(res, wrapper) { - if (!res) { + + make_customer_contact() { + const wrapper = this.dialog.fields_dict["customer_info"].$wrapper; + const contact = this.contact; + const customer = this.contact.links ? this.contact.links[0] : null; + const customer_link = customer ? frappe.utils.get_form_link(customer.link_doctype, customer.link_name, true): ''; + if (!contact) { wrapper.append('Unknown Contact'); } else { wrapper.append(` - -
- ${res.first_name} ${res.last_name} - ${res.mobile_no} - Customer: Some Enterprise +
+ +
+ ${contact.first_name} ${contact.last_name} + ${contact.mobile_no} + ${customer_link} +
`); } @@ -91,11 +102,8 @@ class CallSummaryDialog { } } -$(document).on('app_ready', function() { +$(document).on('app_ready', function () { frappe.realtime.on('incoming_call', data => { - const number = data.CallFrom; - frappe.call_summary_dialog = new CallSummaryDialog({ - number - }); + frappe.call_summary_dialog = new CallSummaryDialog(data); }); }); diff --git a/erpnext/public/less/call_summary.less b/erpnext/public/less/call_summary.less index 73f6fd4f77..0ec2066105 100644 --- a/erpnext/public/less/call_summary.less +++ b/erpnext/public/less/call_summary.less @@ -2,5 +2,6 @@ img { width: auto; height: 100px; + margin-right: 15px; } } \ No newline at end of file From 863b93c32d088ab5616b9265c898641b8762039d Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 21 May 2019 07:57:06 +0530 Subject: [PATCH 05/50] refator: Rename files and move code --- .../crm/call_summary/call_summary_utils.py | 18 ------- .../crm/call_summary/exotel_call_handler.py | 43 ---------------- erpnext/crm/doctype/utils.py | 20 ++++++++ .../exotel_integration.py | 41 +++++++++++++++ .../{call_summary_dialog.js => call_popup.js} | 50 +++++++++---------- 5 files changed, 86 insertions(+), 86 deletions(-) delete mode 100644 erpnext/crm/call_summary/call_summary_utils.py delete mode 100644 erpnext/crm/call_summary/exotel_call_handler.py create mode 100644 erpnext/crm/doctype/utils.py create mode 100644 erpnext/erpnext_integrations/exotel_integration.py rename erpnext/public/js/call_popup/{call_summary_dialog.js => call_popup.js} (75%) diff --git a/erpnext/crm/call_summary/call_summary_utils.py b/erpnext/crm/call_summary/call_summary_utils.py deleted file mode 100644 index 49491d5a4a..0000000000 --- a/erpnext/crm/call_summary/call_summary_utils.py +++ /dev/null @@ -1,18 +0,0 @@ -import frappe - -@frappe.whitelist() -def get_contact_doc(phone_number): - phone_number = phone_number[-10:] - contacts = frappe.get_all('Contact', or_filters={ - 'phone': ['like', '%{}'.format(phone_number)], - 'mobile_no': ['like', '%{}'.format(phone_number)] - }, fields=['name']) - - if contacts: - return frappe.get_doc(contacts[0].name) - -@frappe.whitelist() -def get_last_communication(phone_number, customer=None): - # find last communication through phone_number - # find last issues, opportunity, lead - pass \ No newline at end of file diff --git a/erpnext/crm/call_summary/exotel_call_handler.py b/erpnext/crm/call_summary/exotel_call_handler.py deleted file mode 100644 index a0c3b7d5ea..0000000000 --- a/erpnext/crm/call_summary/exotel_call_handler.py +++ /dev/null @@ -1,43 +0,0 @@ -import frappe - -@frappe.whitelist(allow_guest=True) -def handle_request(*args, **kwargs): - # r = frappe.request - - # print(r.args.to_dict(), args, kwargs) - - incoming_phone_number = kwargs.get('CallFrom') - contact = get_contact_doc(incoming_phone_number) - last_communication = get_last_communication(incoming_phone_number, contact) - - data = { - 'contact': contact, - 'call_payload': kwargs, - 'last_communication': last_communication - } - - frappe.publish_realtime('incoming_call', data) - - -def get_contact_doc(phone_number): - phone_number = phone_number[-10:] - number_filter = { - 'phone': ['like', '%{}'.format(phone_number)], - 'mobile_no': ['like', '%{}'.format(phone_number)] - } - contacts = frappe.get_all('Contact', or_filters=number_filter, - fields=['name'], limit=1) - - if contacts: - return frappe.get_doc('Contact', contacts[0].name) - - leads = frappe.get_all('Leads', or_filters=number_filter, - fields=['name'], limit=1) - - if leads: - return frappe.get_doc('Lead', leads[0].name) - - -def get_last_communication(phone_number, contact): - # frappe.get_all('Communication', filter={}) - return {} diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py new file mode 100644 index 0000000000..3bd92462e3 --- /dev/null +++ b/erpnext/crm/doctype/utils.py @@ -0,0 +1,20 @@ +import frappe + +def get_document_with_phone_number(number): + # finds contacts and leads + number = number[-10:] + number_filter = { + 'phone': ['like', '%{}'.format(number)], + 'mobile_no': ['like', '%{}'.format(number)] + } + contacts = frappe.get_all('Contact', or_filters=number_filter, + fields=['name'], limit=1) + + if contacts: + return frappe.get_doc('Contact', contacts[0].name) + + leads = frappe.get_all('Leads', or_filters=number_filter, + fields=['name'], limit=1) + + if leads: + return frappe.get_doc('Lead', leads[0].name) \ No newline at end of file diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py new file mode 100644 index 0000000000..f8b605a2d1 --- /dev/null +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -0,0 +1,41 @@ +import frappe +from erpnext.crm.doctype.utils import get_document_with_phone_number + +@frappe.whitelist(allow_guest=True) +def handle_incoming_call(*args, **kwargs): + incoming_phone_number = kwargs.get('CallFrom') + + contact = get_document_with_phone_number(incoming_phone_number) + last_communication = get_last_communication(incoming_phone_number, contact) + call_log = create_call_log(kwargs) + data = { + 'contact': contact, + 'call_payload': kwargs, + 'last_communication': last_communication, + 'call_log': call_log + } + + frappe.publish_realtime('incoming_call', data) + + +def get_last_communication(phone_number, contact): + # frappe.get_all('Communication', filter={}) + return {} + +def create_call_log(call_payload): + communication = frappe.new_doc('Communication') + communication.subject = frappe._('Call from {}').format(call_payload.get("CallFrom")) + communication.communication_medium = 'Phone' + communication.send_email = 0 + communication.phone_no = call_payload.get("CallFrom") + communication.comment_type = 'Info' + communication.communication_type = 'Communication' + communication.status = 'Open' + communication.sent_or_received = 'Received' + communication.content = 'call_payload' + communication.communication_date = call_payload.get('StartTime') + # communication.sid = call_payload.get('CallSid') + # communication.exophone = call_payload.get('CallTo') + # communication.call_receiver = call_payload.get('DialWhomNumber') + communication.save(ignore_permissions=True) + return communication diff --git a/erpnext/public/js/call_popup/call_summary_dialog.js b/erpnext/public/js/call_popup/call_popup.js similarity index 75% rename from erpnext/public/js/call_popup/call_summary_dialog.js rename to erpnext/public/js/call_popup/call_popup.js index e9823eeeef..99c2ca38b1 100644 --- a/erpnext/public/js/call_popup/call_summary_dialog.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -1,4 +1,4 @@ -class CallSummaryDialog { +class CallPopup { constructor({ contact, call_payload, last_communication }) { this.number = call_payload.CallFrom; this.contact = contact; @@ -8,7 +8,6 @@ class CallSummaryDialog { make() { this.dialog = new frappe.ui.Dialog({ - 'title': __(`Incoming call from ${this.contact ? this.contact.name : 'Unknown Number'}`), 'static': true, 'minimizable': true, 'fields': [{ @@ -27,40 +26,21 @@ class CallSummaryDialog { 'fieldtype': 'Small Text', 'label': 'Call Summary', 'fieldname': 'call_communication', - 'default': 'This is not working please helpppp', - "placeholder": __("Select or add new customer") }, { 'fieldtype': 'Button', 'label': 'Submit', 'click': () => { - frappe.xcall() + this.dialog.get_value(); } }] }); + this.set_call_status(); this.make_customer_contact(); this.dialog.show(); this.dialog.get_close_btn().show(); this.dialog.header.find('.indicator').removeClass('hidden').addClass('blue'); } - get_dialog_skeleton() { - return ` -
-
-
-
-
-
-
-
-
-
-
-
-
- `; - } - make_customer_contact() { const wrapper = this.dialog.fields_dict["customer_info"].$wrapper; const contact = this.contact; @@ -100,10 +80,30 @@ class CallSummaryDialog { make_summary_section() { // } + + set_call_status(status) { + let title = ''; + if (status === 'incoming') { + if (this.contact) { + title = __('Incoming call from {0}', [this.contact.name]); + } else { + title = __('Incoming call from unknown number'); + } + } + this.dialog.set_title(title); + } + + update(data) { + // pass + } } $(document).on('app_ready', function () { - frappe.realtime.on('incoming_call', data => { - frappe.call_summary_dialog = new CallSummaryDialog(data); + frappe.realtime.on('call_update', data => { + if (!erpnext.call_popup) { + erpnext.call_popup = new CallPopup(data); + } else { + erpnext.call_popup.update(data); + } }); }); From 57bab84198612be61339bc862874cbf4f8ba938f Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 22 May 2019 06:25:45 +0530 Subject: [PATCH 06/50] feat: Add exotel settings --- .../doctype/exotel_settings/__init__.py | 0 .../exotel_settings/exotel_settings.js | 8 +++ .../exotel_settings/exotel_settings.json | 61 +++++++++++++++++++ .../exotel_settings/exotel_settings.py | 21 +++++++ .../exotel_settings/test_exotel_settings.py | 10 +++ 5 files changed, 100 insertions(+) create mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py create mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.js create mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json create mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py create mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/test_exotel_settings.py diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py b/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.js b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.js new file mode 100644 index 0000000000..bfed491d4b --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Exotel Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json new file mode 100644 index 0000000000..72f47b53ec --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json @@ -0,0 +1,61 @@ +{ + "creation": "2019-05-21 07:41:53.536536", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "enabled", + "section_break_2", + "account_sid", + "api_key", + "api_token" + ], + "fields": [ + { + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "depends_on": "enabled", + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "account_sid", + "fieldtype": "Data", + "label": "Account SID" + }, + { + "fieldname": "api_token", + "fieldtype": "Data", + "label": "API Token" + }, + { + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key" + } + ], + "issingle": 1, + "modified": "2019-05-22 06:25:18.026997", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "Exotel Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py new file mode 100644 index 0000000000..763bea0e07 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document +import requests +import frappe +from frappe import _ + +class ExotelSettings(Document): + def validate(self): + self.verify_credentials() + + def verify_credentials(self): + if self.enabled: + response = requests.get('https://api.exotel.com/v1/Accounts/{sid}' + .format(sid = self.account_sid), auth=(self.account_sid, self.api_token)) + if response.status_code != 200: + frappe.throw(_("Invalid credentials")) \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/test_exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/test_exotel_settings.py new file mode 100644 index 0000000000..5d85615c27 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/exotel_settings/test_exotel_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestExotelSettings(unittest.TestCase): + pass From 1eeb89fb778b99411468af25274665f4b4724c83 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 22 May 2019 06:37:43 +0530 Subject: [PATCH 07/50] feat: Add get_call status & make a call function --- .../exotel_settings/exotel_settings.py | 2 +- .../exotel_integration.py | 63 +++++++++++++++---- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py index 763bea0e07..77de84ce5c 100644 --- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py +++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py @@ -16,6 +16,6 @@ class ExotelSettings(Document): def verify_credentials(self): if self.enabled: response = requests.get('https://api.exotel.com/v1/Accounts/{sid}' - .format(sid = self.account_sid), auth=(self.account_sid, self.api_token)) + .format(sid = self.account_sid), auth=(self.api_key, self.api_token)) if response.status_code != 200: frappe.throw(_("Invalid credentials")) \ No newline at end of file diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index f8b605a2d1..3a922f747b 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -1,21 +1,24 @@ import frappe from erpnext.crm.doctype.utils import get_document_with_phone_number +import requests + +# api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call @frappe.whitelist(allow_guest=True) def handle_incoming_call(*args, **kwargs): - incoming_phone_number = kwargs.get('CallFrom') + # incoming_phone_number = kwargs.get('CallFrom') - contact = get_document_with_phone_number(incoming_phone_number) - last_communication = get_last_communication(incoming_phone_number, contact) + # contact = get_document_with_phone_number(incoming_phone_number) + # last_communication = get_last_communication(incoming_phone_number, contact) call_log = create_call_log(kwargs) - data = { - 'contact': contact, - 'call_payload': kwargs, - 'last_communication': last_communication, + data = frappe._dict({ + 'call_from': kwargs.get('CallFrom'), + 'agent_email': kwargs.get('AgentEmail'), + 'call_type': kwargs.get('Direction'), 'call_log': call_log - } + }) - frappe.publish_realtime('incoming_call', data) + frappe.publish_realtime('show_call_popup', data, user=data.agent_email) def get_last_communication(phone_number, contact): @@ -23,6 +26,17 @@ def get_last_communication(phone_number, contact): return {} def create_call_log(call_payload): + communication = frappe.get_all('Communication', { + 'communication_medium': 'Phone', + 'call_id': call_payload.get('CallSid'), + }, limit=1) + + if communication: + log = frappe.get_doc('Communication', communication[0].name) + log.call_status = 'Connected' + log.save(ignore_permissions=True) + return log + communication = frappe.new_doc('Communication') communication.subject = frappe._('Call from {}').format(call_payload.get("CallFrom")) communication.communication_medium = 'Phone' @@ -33,9 +47,34 @@ def create_call_log(call_payload): communication.status = 'Open' communication.sent_or_received = 'Received' communication.content = 'call_payload' + communication.call_status = 'Incoming' communication.communication_date = call_payload.get('StartTime') - # communication.sid = call_payload.get('CallSid') - # communication.exophone = call_payload.get('CallTo') - # communication.call_receiver = call_payload.get('DialWhomNumber') + communication.call_id = call_payload.get('CallSid') communication.save(ignore_permissions=True) return communication + +def get_call_status(call_id): + settings = get_exotel_settings() + response = requests.get('https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/erpnext/{sid}/{call_id}.json'.format( + api_key=settings.api_key, + api_token=settings.api_token, + call_id=call_id + )) + return response.json() + +@frappe.whitelist(allow_guest=True) +def make_a_call(from_number, to_number, caller_id): + settings = get_exotel_settings() + response = requests.post('https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/Calls/connect.json'.format( + api_key=settings.api_key, + api_token=settings.api_token, + ), data={ + 'From': from_number, + 'To': to_number, + 'CallerId': caller_id + }) + + return response.json() + +def get_exotel_settings(): + return frappe.get_single('Exotel Settings') \ No newline at end of file From fb1964aa18192a6c1091e4a80656f8bd71ce9f82 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 22 May 2019 06:38:25 +0530 Subject: [PATCH 08/50] fix: Changes to fix popup --- erpnext/public/build.json | 5 ++- erpnext/public/js/call_popup/call_popup.js | 36 +++++++------------ .../{call_summary.less => call_popup.less} | 0 3 files changed, 14 insertions(+), 27 deletions(-) rename erpnext/public/less/{call_summary.less => call_popup.less} (100%) diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 3f55d0737d..818f336d9c 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -2,7 +2,7 @@ "css/erpnext.css": [ "public/less/erpnext.less", "public/less/hub.less", - "public/less/call_summary.less" + "public/less/call_popup.less" ], "css/marketplace.css": [ "public/less/hub.less" @@ -50,8 +50,7 @@ "public/js/education/student_button.html", "public/js/education/assessment_result_tool.html", "public/js/hub/hub_factory.js", - "public/js/call_popup/call_summary_dialog.js", - "public/js/call_popup/call_summary.html" + "public/js/call_popup/call_popup.js" ], "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 99c2ca38b1..2d95c5db72 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -1,8 +1,7 @@ class CallPopup { - constructor({ contact, call_payload, last_communication }) { - this.number = call_payload.CallFrom; - this.contact = contact; - this.last_communication = last_communication; + constructor({ call_from, call_log }) { + this.number = call_from; + this.call_log = call_log; this.make(); } @@ -34,7 +33,7 @@ class CallPopup { } }] }); - this.set_call_status(); + this.set_call_status(this.call_log.call_status); this.make_customer_contact(); this.dialog.show(); this.dialog.get_close_btn().show(); @@ -62,48 +61,37 @@ class CallPopup { } } - make_last_communication_section() { - const last_communication_section = this.body.find('.last-communication'); - const last_communication = frappe.ui.form.make_control({ - parent: last_communication_section, - df: { - fieldtype: "Text", - label: "Last Communication", - fieldname: "last_communication", - 'default': 'This is not working please helpppp', - "placeholder": __("Select or add new customer") - }, - }); - last_communication.set_value('This is not working please helpppp'); - } - make_summary_section() { // } - set_call_status(status) { + set_call_status() { let title = ''; - if (status === 'incoming') { + if (this.call_log.call_status === 'Incoming') { if (this.contact) { title = __('Incoming call from {0}', [this.contact.name]); } else { title = __('Incoming call from unknown number'); } + } else { + title = __('Call Connected'); } this.dialog.set_title(title); } update(data) { - // pass + this.call_log = data.call_log; + this.set_call_status(); } } $(document).on('app_ready', function () { - frappe.realtime.on('call_update', data => { + frappe.realtime.on('show_call_popup', data => { if (!erpnext.call_popup) { erpnext.call_popup = new CallPopup(data); } else { erpnext.call_popup.update(data); + erpnext.call_popup.dialog.show(); } }); }); diff --git a/erpnext/public/less/call_summary.less b/erpnext/public/less/call_popup.less similarity index 100% rename from erpnext/public/less/call_summary.less rename to erpnext/public/less/call_popup.less From 07fe299628106d00ac7d5e53778d9431d846ad23 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 22 May 2019 15:48:57 +0530 Subject: [PATCH 09/50] fix: Make changes to fix call flow and popup trigger --- erpnext/crm/doctype/utils.py | 15 ++- .../exotel_integration.py | 69 +++++++----- erpnext/public/js/call_popup/call_popup.js | 104 ++++++++++++------ 3 files changed, 121 insertions(+), 67 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 3bd92462e3..5c338174bc 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -1,20 +1,25 @@ import frappe +@frappe.whitelist() def get_document_with_phone_number(number): # finds contacts and leads + if not number: return number = number[-10:] number_filter = { 'phone': ['like', '%{}'.format(number)], 'mobile_no': ['like', '%{}'.format(number)] } - contacts = frappe.get_all('Contact', or_filters=number_filter, - fields=['name'], limit=1) + contacts = frappe.get_all('Contact', or_filters=number_filter, limit=1) if contacts: return frappe.get_doc('Contact', contacts[0].name) - leads = frappe.get_all('Leads', or_filters=number_filter, - fields=['name'], limit=1) + leads = frappe.get_all('Lead', or_filters=number_filter, limit=1) if leads: - return frappe.get_doc('Lead', leads[0].name) \ No newline at end of file + return frappe.get_doc('Lead', leads[0].name) + + +def get_customer_last_interaction(contact_doc): + # + pass \ No newline at end of file diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 3a922f747b..c45945fc4d 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -6,66 +6,77 @@ import requests @frappe.whitelist(allow_guest=True) def handle_incoming_call(*args, **kwargs): - # incoming_phone_number = kwargs.get('CallFrom') + exotel_settings = get_exotel_settings() + if not exotel_settings.enabled: return + + employee_email = kwargs.get('AgentEmail') + status = kwargs.get('Status') + + if status == 'free' and get_call_status(kwargs.get('CallSid')) == ['ringing', 'in-progress']: + # redirected to other agent number + frappe.publish_realtime('terminate_call_popup', user=employee_email) + return + + call_log = get_call_log(kwargs) - # contact = get_document_with_phone_number(incoming_phone_number) - # last_communication = get_last_communication(incoming_phone_number, contact) - call_log = create_call_log(kwargs) data = frappe._dict({ 'call_from': kwargs.get('CallFrom'), 'agent_email': kwargs.get('AgentEmail'), 'call_type': kwargs.get('Direction'), - 'call_log': call_log + 'call_log': call_log, + 'call_status_method': 'erpnext.erpnext_integrations.exotel_integration.get_call_status' }) - - frappe.publish_realtime('show_call_popup', data, user=data.agent_email) - + if call_log.call_status in ['ringing', 'in-progress']: + frappe.publish_realtime('show_call_popup', data, user=data.agent_email) def get_last_communication(phone_number, contact): # frappe.get_all('Communication', filter={}) return {} -def create_call_log(call_payload): +def get_call_log(call_payload): communication = frappe.get_all('Communication', { 'communication_medium': 'Phone', 'call_id': call_payload.get('CallSid'), }, limit=1) if communication: - log = frappe.get_doc('Communication', communication[0].name) - log.call_status = 'Connected' - log.save(ignore_permissions=True) - return log + communication = frappe.get_doc('Communication', communication[0].name) + else: + communication = frappe.new_doc('Communication') + communication.subject = frappe._('Call from {}').format(call_payload.get("CallFrom")) + communication.communication_medium = 'Phone' + communication.phone_no = call_payload.get("CallFrom") + communication.comment_type = 'Info' + communication.communication_type = 'Communication' + communication.sent_or_received = 'Received' + communication.communication_date = call_payload.get('StartTime') + communication.call_id = call_payload.get('CallSid') - communication = frappe.new_doc('Communication') - communication.subject = frappe._('Call from {}').format(call_payload.get("CallFrom")) - communication.communication_medium = 'Phone' - communication.send_email = 0 - communication.phone_no = call_payload.get("CallFrom") - communication.comment_type = 'Info' - communication.communication_type = 'Communication' - communication.status = 'Open' - communication.sent_or_received = 'Received' + status = get_call_status(communication.call_id) + communication.call_status = status or 'failed' + communication.status = 'Closed' if status in ['completed', 'failed', 'no-answer'] else 'Open' + communication.call_duration = call_payload.get('Duration') if status in ['completed', 'failed', 'no-answer'] else 0 communication.content = 'call_payload' - communication.call_status = 'Incoming' - communication.communication_date = call_payload.get('StartTime') - communication.call_id = call_payload.get('CallSid') communication.save(ignore_permissions=True) + frappe.db.commit() return communication +@frappe.whitelist() def get_call_status(call_id): + print(call_id) settings = get_exotel_settings() - response = requests.get('https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/erpnext/{sid}/{call_id}.json'.format( + response = requests.get('https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/erpnext/Calls/{call_id}.json'.format( api_key=settings.api_key, api_token=settings.api_token, call_id=call_id )) - return response.json() + status = response.json().get('Call', {}).get('Status') + return status -@frappe.whitelist(allow_guest=True) +@frappe.whitelist() def make_a_call(from_number, to_number, caller_id): settings = get_exotel_settings() - response = requests.post('https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/Calls/connect.json'.format( + response = requests.post('https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/Calls/connect.json?details=true'.format( api_key=settings.api_key, api_token=settings.api_token, ), data={ diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 2d95c5db72..7236f9e828 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -1,8 +1,11 @@ class CallPopup { - constructor({ call_from, call_log }) { + constructor({ call_from, call_log, call_status_method }) { this.number = call_from; this.call_log = call_log; + this.call_status_method = call_status_method; this.make(); + this.make_customer_contact(); + this.setup_call_status_updater(); } make() { @@ -34,47 +37,54 @@ class CallPopup { }] }); this.set_call_status(this.call_log.call_status); - this.make_customer_contact(); this.dialog.show(); this.dialog.get_close_btn().show(); - this.dialog.header.find('.indicator').removeClass('hidden').addClass('blue'); } make_customer_contact() { const wrapper = this.dialog.fields_dict["customer_info"].$wrapper; - const contact = this.contact; - const customer = this.contact.links ? this.contact.links[0] : null; - const customer_link = customer ? frappe.utils.get_form_link(customer.link_doctype, customer.link_name, true): ''; - if (!contact) { - wrapper.append('Unknown Contact'); - } else { - wrapper.append(` -
- -
- ${contact.first_name} ${contact.last_name} - ${contact.mobile_no} - ${customer_link} -
-
- `); - } - } - - make_summary_section() { - // - } - - set_call_status() { - let title = ''; - if (this.call_log.call_status === 'Incoming') { - if (this.contact) { - title = __('Incoming call from {0}', [this.contact.name]); + wrapper.append('
Loading...
'); + frappe.xcall('erpnext.crm.doctype.utils.get_document_with_phone_number', { + 'number': this.number + }).then(contact_doc => { + wrapper.empty(); + const contact = contact_doc; + if (!contact) { + wrapper.append('
Unknown Contact
'); + wrapper.append(`${__('Make New Contact')}`); } else { - title = __('Incoming call from unknown number'); + const link = contact.links ? contact.links[0] : null; + const contact_link = link ? frappe.utils.get_form_link(link.link_doctype, link.link_name, true): ''; + wrapper.append(` +
+ +
+ ${contact.first_name} ${contact.last_name} + ${contact.mobile_no} + ${contact_link} +
+
+ `); } - } else { + }); + } + + set_indicator(color) { + this.dialog.header.find('.indicator').removeClass('hidden').addClass('blink').addClass(color); + } + + set_call_status(call_status) { + let title = ''; + call_status = this.call_log.call_status; + if (call_status === 'busy') { + title = __('Incoming call'); + this.set_indicator('blue'); + } else if (call_status === 'in-progress') { title = __('Call Connected'); + this.set_indicator('yellow'); + } else if (call_status === 'missed') { + this.set_indicator('red'); + title = __('Call Missed'); } this.dialog.set_title(title); } @@ -83,6 +93,27 @@ class CallPopup { this.call_log = data.call_log; this.set_call_status(); } + + setup_call_status_updater() { + this.updater = setInterval(this.get_call_status.bind(this), 2000); + } + + get_call_status() { + frappe.xcall(this.call_status_method, { + 'call_id': this.call_log.call_id + }).then((call_status) => { + if (call_status === 'completed') { + clearInterval(this.updater); + } + }); + } + + terminate_popup() { + clearInterval(this.updater); + this.dialog.hide(); + delete erpnext.call_popup; + frappe.msgprint('Call Forwarded'); + } } $(document).on('app_ready', function () { @@ -90,8 +121,15 @@ $(document).on('app_ready', function () { if (!erpnext.call_popup) { erpnext.call_popup = new CallPopup(data); } else { + console.log(data); erpnext.call_popup.update(data); erpnext.call_popup.dialog.show(); } }); + + frappe.realtime.on('terminate_call_popup', () => { + if (erpnext.call_popup) { + erpnext.call_popup.terminate_popup(); + } + }); }); From 591ad3789454b096b8d37ac65e5bed4dbda4152d Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 24 May 2019 10:11:48 +0530 Subject: [PATCH 10/50] fix: call popup [wip] --- erpnext/crm/doctype/utils.py | 49 ++++++++++- erpnext/public/js/call_popup/call_popup.js | 94 +++++++++++++++++++--- erpnext/public/less/call_popup.less | 3 + 3 files changed, 131 insertions(+), 15 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 5c338174bc..36cb0c1f23 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -1,4 +1,5 @@ import frappe +import json @frappe.whitelist() def get_document_with_phone_number(number): @@ -19,7 +20,49 @@ def get_document_with_phone_number(number): if leads: return frappe.get_doc('Lead', leads[0].name) +@frappe.whitelist() +def get_last_interaction(number, reference_doc): + reference_doc = json.loads(reference_doc) if reference_doc else get_document_with_phone_number(number) -def get_customer_last_interaction(contact_doc): - # - pass \ No newline at end of file + if not reference_doc: return + + reference_doc = frappe._dict(reference_doc) + + last_communication = {} + last_issue = {} + if reference_doc.doctype == 'Contact': + customer_name = '' + query_condition = '' + for link in reference_doc.links: + link = frappe._dict(link) + if link.link_doctype == 'Customer': + customer_name = link.link_name + query_condition += "(`reference_doctype`='{}' AND `reference_name`='{}') OR".format(link.link_doctype, link.link_name) + + if query_condition: + query_condition = query_condition[:-2] + + last_communication = frappe.db.sql(""" + SELECT `name`, `content` + FROM `tabCommunication` + WHERE {} + ORDER BY `modified` + LIMIT 1 + """.format(query_condition)) + + if customer_name: + last_issue = frappe.get_all('Issue', { + 'customer': customer_name + }, ['name', 'subject'], limit=1) + + elif reference_doc.doctype == 'Lead': + last_communication = frappe.get_all('Communication', { + 'reference_doctype': reference_doc.doctype, + 'reference_name': reference_doc.name, + 'sent_or_received': 'Received' + }, fields=['name', 'content'], limit=1) + + return { + 'last_communication': last_communication[0] if last_communication else None, + 'last_issue': last_issue[0] if last_issue else None + } \ No newline at end of file diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 7236f9e828..38464785db 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -4,8 +4,6 @@ class CallPopup { this.call_log = call_log; this.call_status_method = call_status_method; this.make(); - this.make_customer_contact(); - this.setup_call_status_updater(); } make() { @@ -16,29 +14,68 @@ class CallPopup { 'fieldname': 'customer_info', 'fieldtype': 'HTML' }, { - 'fieldtype': 'Section Break' + 'label': 'Last Interaction', + 'fielname': 'last_interaction', + 'fieldtype': 'Section Break', + // 'hidden': true }, { 'fieldtype': 'Small Text', 'label': "Last Communication", 'fieldname': 'last_communication', - 'read_only': true }, { - 'fieldtype': 'Column Break' + 'fieldname': 'last_communication_link', + 'fieldtype': 'HTML', + }, { + 'fieldtype': 'Small Text', + 'label': "Last Issue", + 'fieldname': 'last_issue', + }, { + 'fieldname': 'last_issue_link', + 'fieldtype': 'HTML', + }, { + 'label': 'Enter Call Summary', + 'fieldtype': 'Section Break', }, { 'fieldtype': 'Small Text', 'label': 'Call Summary', - 'fieldname': 'call_communication', + 'fieldname': 'call_summary', + }, { + 'label': 'Append To', + 'fieldtype': 'Select', + 'fieldname': 'doctype', + 'options': ['Issue', 'Lead', 'Communication'], + 'default': this.call_log.doctype + }, { + 'label': 'Document', + 'fieldtype': 'Dynamic Link', + 'fieldname': 'docname', + 'options': 'doctype', + 'default': this.call_log.name }, { 'fieldtype': 'Button', 'label': 'Submit', 'click': () => { - this.dialog.get_value(); + const values = this.dialog.get_values(); + frappe.xcall('frappe.desk.form.utils.add_comment', { + 'reference_doctype': values.doctype, + 'reference_name': values.docname, + 'content': `${__('Call Summary')}: ${values.call_summary}`, + 'comment_email': frappe.session.user + }).then(() => { + this.dialog.set_value('call_summary', ''); + }); } }] }); this.set_call_status(this.call_log.call_status); - this.dialog.show(); + this.make_customer_contact(); this.dialog.get_close_btn().show(); + this.setup_call_status_updater(); + this.dialog.set_secondary_action(() => { + clearInterval(this.updater); + this.dialog.hide(); + }); + this.dialog.show(); } make_customer_contact() { @@ -48,10 +85,16 @@ class CallPopup { 'number': this.number }).then(contact_doc => { wrapper.empty(); - const contact = contact_doc; + const contact = this.contact = contact_doc; if (!contact) { - wrapper.append('
Unknown Contact
'); - wrapper.append(`${__('Make New Contact')}`); + wrapper.append(` +
+
Unknown Number: ${this.number}
+ + ${__('Create New Contact')} + +
+ `); } else { const link = contact.links ? contact.links[0] : null; const contact_link = link ? frappe.utils.get_form_link(link.link_doctype, link.link_name, true): ''; @@ -65,6 +108,7 @@ class CallPopup {
`); + this.make_last_interaction_section(); } }); } @@ -85,6 +129,9 @@ class CallPopup { } else if (call_status === 'missed') { this.set_indicator('red'); title = __('Call Missed'); + } else { + this.set_indicator('blue'); + title = call_status; } this.dialog.set_title(title); } @@ -95,7 +142,7 @@ class CallPopup { } setup_call_status_updater() { - this.updater = setInterval(this.get_call_status.bind(this), 2000); + this.updater = setInterval(this.get_call_status.bind(this), 20000); } get_call_status() { @@ -114,6 +161,29 @@ class CallPopup { delete erpnext.call_popup; frappe.msgprint('Call Forwarded'); } + + make_last_interaction_section() { + frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', { + 'number': this.number, + 'reference_doc': this.contact + }).then(data => { + if (data.last_communication) { + const comm = data.last_communication; + // this.dialog.set_df_property('last_interaction', 'hidden', false); + const comm_field = this.dialog.fields_dict["last_communication"]; + comm_field.set_value(comm.content); + comm_field.$wrapper.append(frappe.utils.get_form_link('Communication', comm.name)); + } + + if (data.last_issue) { + const issue = data.last_issue; + // this.dialog.set_df_property('last_interaction', 'hidden', false); + const issue_field = this.dialog.fields_dict["last_issue"]; + issue_field.set_value(issue.subject); + issue_field.$wrapper.append(frappe.utils.get_form_link('Issue', issue.name, true)); + } + }); + } } $(document).on('app_ready', function () { diff --git a/erpnext/public/less/call_popup.less b/erpnext/public/less/call_popup.less index 0ec2066105..6ec2db9cd8 100644 --- a/erpnext/public/less/call_popup.less +++ b/erpnext/public/less/call_popup.less @@ -4,4 +4,7 @@ height: 100px; margin-right: 15px; } + a:hover { + text-decoration: underline; + } } \ No newline at end of file From 39a4d59cf60a8b624221787f4c0c9812779de675 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 27 May 2019 10:38:43 +0530 Subject: [PATCH 11/50] fix: Call popup modal --- .../exotel_integration.py | 10 ++- erpnext/public/js/call_popup/call_popup.js | 65 ++++++++++--------- erpnext/public/less/call_popup.less | 5 +- 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index c45945fc4d..39d43b368f 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -12,9 +12,10 @@ def handle_incoming_call(*args, **kwargs): employee_email = kwargs.get('AgentEmail') status = kwargs.get('Status') - if status == 'free' and get_call_status(kwargs.get('CallSid')) == ['ringing', 'in-progress']: - # redirected to other agent number - frappe.publish_realtime('terminate_call_popup', user=employee_email) + if status == 'free': + # call disconnected for agent + # "and get_call_status(kwargs.get('CallSid')) in ['in-progress']" - additional check to ensure if the call was redirected + frappe.publish_realtime('call_disconnected', user=employee_email) return call_log = get_call_log(kwargs) @@ -29,9 +30,6 @@ def handle_incoming_call(*args, **kwargs): if call_log.call_status in ['ringing', 'in-progress']: frappe.publish_realtime('show_call_popup', data, user=data.agent_email) -def get_last_communication(phone_number, contact): - # frappe.get_all('Communication', filter={}) - return {} def get_call_log(call_payload): communication = frappe.get_all('Communication', { diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 38464785db..f203c8e855 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -1,6 +1,6 @@ class CallPopup { constructor({ call_from, call_log, call_status_method }) { - this.number = call_from; + this.caller_number = call_from; this.call_log = call_log; this.call_status_method = call_status_method; this.make(); @@ -11,17 +11,16 @@ class CallPopup { 'static': true, 'minimizable': true, 'fields': [{ - 'fieldname': 'customer_info', + 'fieldname': 'caller_info', 'fieldtype': 'HTML' }, { - 'label': 'Last Interaction', 'fielname': 'last_interaction', 'fieldtype': 'Section Break', - // 'hidden': true }, { 'fieldtype': 'Small Text', 'label': "Last Communication", 'fieldname': 'last_communication', + 'read_only': true }, { 'fieldname': 'last_communication_link', 'fieldtype': 'HTML', @@ -29,12 +28,12 @@ class CallPopup { 'fieldtype': 'Small Text', 'label': "Last Issue", 'fieldname': 'last_issue', + 'read_only': true }, { 'fieldname': 'last_issue_link', 'fieldtype': 'HTML', }, { - 'label': 'Enter Call Summary', - 'fieldtype': 'Section Break', + 'fieldtype': 'Column Break', }, { 'fieldtype': 'Small Text', 'label': 'Call Summary', @@ -65,10 +64,13 @@ class CallPopup { this.dialog.set_value('call_summary', ''); }); } - }] + }], + on_minimize_toggle: () => { + this.set_call_status(); + } }); this.set_call_status(this.call_log.call_status); - this.make_customer_contact(); + this.make_caller_info_section(); this.dialog.get_close_btn().show(); this.setup_call_status_updater(); this.dialog.set_secondary_action(() => { @@ -78,19 +80,19 @@ class CallPopup { this.dialog.show(); } - make_customer_contact() { - const wrapper = this.dialog.fields_dict["customer_info"].$wrapper; + make_caller_info_section() { + const wrapper = this.dialog.fields_dict['caller_info'].$wrapper; wrapper.append('
Loading...
'); frappe.xcall('erpnext.crm.doctype.utils.get_document_with_phone_number', { - 'number': this.number + 'number': this.caller_number }).then(contact_doc => { wrapper.empty(); const contact = this.contact = contact_doc; if (!contact) { wrapper.append(` -
-
Unknown Number: ${this.number}
- + @@ -99,7 +101,7 @@ class CallPopup { const link = contact.links ? contact.links[0] : null; const contact_link = link ? frappe.utils.get_form_link(link.link_doctype, link.link_name, true): ''; wrapper.append(` -
+
${contact.first_name} ${contact.last_name} @@ -108,27 +110,32 @@ class CallPopup {
`); + this.set_call_status(); this.make_last_interaction_section(); } }); } - set_indicator(color) { - this.dialog.header.find('.indicator').removeClass('hidden').addClass('blink').addClass(color); + set_indicator(color, blink=false) { + this.dialog.header.find('.indicator').removeClass('hidden').toggleClass('blink', blink).addClass(color); } set_call_status(call_status) { let title = ''; - call_status = this.call_log.call_status; - if (call_status === 'busy') { - title = __('Incoming call'); - this.set_indicator('blue'); + call_status = call_status || this.call_log.call_status; + if (['busy', 'completed'].includes(call_status)) { + title = __('Incoming call from {0}', + [this.contact ? `${this.contact.first_name} ${this.contact.last_name}` : this.caller_number]); + this.set_indicator('blue', true); } else if (call_status === 'in-progress') { title = __('Call Connected'); this.set_indicator('yellow'); } else if (call_status === 'missed') { this.set_indicator('red'); title = __('Call Missed'); + } else if (call_status === 'disconnected') { + this.set_indicator('red'); + title = __('Call Disconnected'); } else { this.set_indicator('blue'); title = call_status; @@ -155,16 +162,14 @@ class CallPopup { }); } - terminate_popup() { + disconnect_call() { + this.set_call_status('disconnected'); clearInterval(this.updater); - this.dialog.hide(); - delete erpnext.call_popup; - frappe.msgprint('Call Forwarded'); } make_last_interaction_section() { frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', { - 'number': this.number, + 'number': this.caller_number, 'reference_doc': this.contact }).then(data => { if (data.last_communication) { @@ -180,7 +185,8 @@ class CallPopup { // this.dialog.set_df_property('last_interaction', 'hidden', false); const issue_field = this.dialog.fields_dict["last_issue"]; issue_field.set_value(issue.subject); - issue_field.$wrapper.append(frappe.utils.get_form_link('Issue', issue.name, true)); + issue_field.$wrapper + .append(`View ${issue.name}`); } }); } @@ -191,15 +197,14 @@ $(document).on('app_ready', function () { if (!erpnext.call_popup) { erpnext.call_popup = new CallPopup(data); } else { - console.log(data); erpnext.call_popup.update(data); erpnext.call_popup.dialog.show(); } }); - frappe.realtime.on('terminate_call_popup', () => { + frappe.realtime.on('call_disconnected', () => { if (erpnext.call_popup) { - erpnext.call_popup.terminate_popup(); + erpnext.call_popup.disconnect_call(); } }); }); diff --git a/erpnext/public/less/call_popup.less b/erpnext/public/less/call_popup.less index 6ec2db9cd8..3f4ffef3c0 100644 --- a/erpnext/public/less/call_popup.less +++ b/erpnext/public/less/call_popup.less @@ -1,4 +1,7 @@ -.customer-info { +.call-popup { + .caller-info { + padding: 0 15px; + } img { width: auto; height: 100px; From bd03a51e8fe9b2027b1e1bbb3ab6fccc3a858fa3 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 27 May 2019 15:30:41 +0530 Subject: [PATCH 12/50] fix: Handle end call --- .../exotel_integration.py | 31 +++++++++++-------- erpnext/public/js/call_popup/call_popup.js | 3 +- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 39d43b368f..5b24e7c138 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -27,11 +27,19 @@ def handle_incoming_call(*args, **kwargs): 'call_log': call_log, 'call_status_method': 'erpnext.erpnext_integrations.exotel_integration.get_call_status' }) - if call_log.call_status in ['ringing', 'in-progress']: - frappe.publish_realtime('show_call_popup', data, user=data.agent_email) + + frappe.publish_realtime('show_call_popup', data, user=data.agent_email) + +@frappe.whitelist(allow_guest=True) +def handle_end_call(*args, **kwargs): + call_log = get_call_log(kwargs) + if call_log: + call_log.status = 'Closed' + call_log.save(ignore_permissions=True) + frappe.db.commit() -def get_call_log(call_payload): +def get_call_log(call_payload, create_new_if_not_found=True): communication = frappe.get_all('Communication', { 'communication_medium': 'Phone', 'call_id': call_payload.get('CallSid'), @@ -39,7 +47,8 @@ def get_call_log(call_payload): if communication: communication = frappe.get_doc('Communication', communication[0].name) - else: + return communication + elif create_new_if_not_found: communication = frappe.new_doc('Communication') communication.subject = frappe._('Call from {}').format(call_payload.get("CallFrom")) communication.communication_medium = 'Phone' @@ -49,15 +58,11 @@ def get_call_log(call_payload): communication.sent_or_received = 'Received' communication.communication_date = call_payload.get('StartTime') communication.call_id = call_payload.get('CallSid') - - status = get_call_status(communication.call_id) - communication.call_status = status or 'failed' - communication.status = 'Closed' if status in ['completed', 'failed', 'no-answer'] else 'Open' - communication.call_duration = call_payload.get('Duration') if status in ['completed', 'failed', 'no-answer'] else 0 - communication.content = 'call_payload' - communication.save(ignore_permissions=True) - frappe.db.commit() - return communication + communication.status = 'Open' + communication.content = frappe._('Call from {}').format(call_payload.get("CallFrom")) + communication.save(ignore_permissions=True) + frappe.db.commit() + return communication @frappe.whitelist() def get_call_status(call_id): diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index f203c8e855..3fa5fa63c6 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -73,6 +73,7 @@ class CallPopup { this.make_caller_info_section(); this.dialog.get_close_btn().show(); this.setup_call_status_updater(); + this.dialog.$body.addClass('call-popup'); this.dialog.set_secondary_action(() => { clearInterval(this.updater); this.dialog.hide(); @@ -123,7 +124,7 @@ class CallPopup { set_call_status(call_status) { let title = ''; call_status = call_status || this.call_log.call_status; - if (['busy', 'completed'].includes(call_status)) { + if (['busy', 'completed'].includes(call_status) || !call_status) { title = __('Incoming call from {0}', [this.contact ? `${this.contact.first_name} ${this.contact.last_name}` : this.caller_number]); this.set_indicator('blue', true); From b1a55a2af5b3f2767bc5ea22ed32952a1648af14 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 27 May 2019 16:18:50 +0530 Subject: [PATCH 13/50] fix: Add call summary --- erpnext/crm/doctype/utils.py | 12 +++++++++++- erpnext/public/js/call_popup/call_popup.js | 20 +++----------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 36cb0c1f23..9399fba40f 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -65,4 +65,14 @@ def get_last_interaction(number, reference_doc): return { 'last_communication': last_communication[0] if last_communication else None, 'last_issue': last_issue[0] if last_issue else None - } \ No newline at end of file + } + +@frappe.whitelist() +def add_call_summary(docname, summary): + communication = frappe.get_doc('Communication', docname) + communication.content = 'Call Summary by {user}: {summary}'.format({ + 'user': frappe.utils.get_fullname(frappe.session.user), + 'summary': summary + }) + communication.save(ignore_permissions=True) + diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 3fa5fa63c6..2410684429 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -38,28 +38,14 @@ class CallPopup { 'fieldtype': 'Small Text', 'label': 'Call Summary', 'fieldname': 'call_summary', - }, { - 'label': 'Append To', - 'fieldtype': 'Select', - 'fieldname': 'doctype', - 'options': ['Issue', 'Lead', 'Communication'], - 'default': this.call_log.doctype - }, { - 'label': 'Document', - 'fieldtype': 'Dynamic Link', - 'fieldname': 'docname', - 'options': 'doctype', - 'default': this.call_log.name }, { 'fieldtype': 'Button', 'label': 'Submit', 'click': () => { const values = this.dialog.get_values(); - frappe.xcall('frappe.desk.form.utils.add_comment', { - 'reference_doctype': values.doctype, - 'reference_name': values.docname, - 'content': `${__('Call Summary')}: ${values.call_summary}`, - 'comment_email': frappe.session.user + frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', { + 'docname': this.call_log.name, + 'summary': `${__('Call Summary')}: ${values.call_summary}`, }).then(() => { this.dialog.set_value('call_summary', ''); }); From 893a0c24bbdb64173fa27d125e22270c61c9fba3 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 31 May 2019 13:42:22 +0530 Subject: [PATCH 14/50] fix: Add call summary --- erpnext/crm/doctype/utils.py | 12 +++++++++++- erpnext/public/js/call_popup/call_popup.js | 20 +++----------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 36cb0c1f23..9399fba40f 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -65,4 +65,14 @@ def get_last_interaction(number, reference_doc): return { 'last_communication': last_communication[0] if last_communication else None, 'last_issue': last_issue[0] if last_issue else None - } \ No newline at end of file + } + +@frappe.whitelist() +def add_call_summary(docname, summary): + communication = frappe.get_doc('Communication', docname) + communication.content = 'Call Summary by {user}: {summary}'.format({ + 'user': frappe.utils.get_fullname(frappe.session.user), + 'summary': summary + }) + communication.save(ignore_permissions=True) + diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 3fa5fa63c6..2410684429 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -38,28 +38,14 @@ class CallPopup { 'fieldtype': 'Small Text', 'label': 'Call Summary', 'fieldname': 'call_summary', - }, { - 'label': 'Append To', - 'fieldtype': 'Select', - 'fieldname': 'doctype', - 'options': ['Issue', 'Lead', 'Communication'], - 'default': this.call_log.doctype - }, { - 'label': 'Document', - 'fieldtype': 'Dynamic Link', - 'fieldname': 'docname', - 'options': 'doctype', - 'default': this.call_log.name }, { 'fieldtype': 'Button', 'label': 'Submit', 'click': () => { const values = this.dialog.get_values(); - frappe.xcall('frappe.desk.form.utils.add_comment', { - 'reference_doctype': values.doctype, - 'reference_name': values.docname, - 'content': `${__('Call Summary')}: ${values.call_summary}`, - 'comment_email': frappe.session.user + frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', { + 'docname': this.call_log.name, + 'summary': `${__('Call Summary')}: ${values.call_summary}`, }).then(() => { this.dialog.set_value('call_summary', ''); }); From e9bfecf4052353934c2193e1779e5c9e536c4935 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 3 Jun 2019 12:27:02 +0530 Subject: [PATCH 15/50] fix: Add code to update call summary --- erpnext/crm/doctype/utils.py | 13 +++++--- .../exotel_integration.py | 32 +++++++++++++++++-- erpnext/public/js/call_popup/call_popup.js | 6 ++-- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 9399fba40f..26cb2981d8 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -1,4 +1,5 @@ import frappe +from frappe import _ import json @frappe.whitelist() @@ -53,7 +54,7 @@ def get_last_interaction(number, reference_doc): if customer_name: last_issue = frappe.get_all('Issue', { 'customer': customer_name - }, ['name', 'subject'], limit=1) + }, ['name', 'subject', 'customer'], limit=1) elif reference_doc.doctype == 'Lead': last_communication = frappe.get_all('Communication', { @@ -70,9 +71,11 @@ def get_last_interaction(number, reference_doc): @frappe.whitelist() def add_call_summary(docname, summary): communication = frappe.get_doc('Communication', docname) - communication.content = 'Call Summary by {user}: {summary}'.format({ - 'user': frappe.utils.get_fullname(frappe.session.user), - 'summary': summary - }) + content = _('Call Summary by {0}: {1}').format( + frappe.utils.get_fullname(frappe.session.user), summary) + if not communication.content: + communication.content = content + else: + communication.content += '\n' + content communication.save(ignore_permissions=True) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 5b24e7c138..c70b0948ae 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -32,7 +32,14 @@ def handle_incoming_call(*args, **kwargs): @frappe.whitelist(allow_guest=True) def handle_end_call(*args, **kwargs): - call_log = get_call_log(kwargs) + close_call_log(kwargs) + +@frappe.whitelist(allow_guest=True) +def handle_missed_call(*args, **kwargs): + close_call_log(kwargs) + +def close_call_log(call_payload): + call_log = get_call_log(call_payload) if call_log: call_log.status = 'Closed' call_log.save(ignore_permissions=True) @@ -82,6 +89,7 @@ def make_a_call(from_number, to_number, caller_id): response = requests.post('https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/Calls/connect.json?details=true'.format( api_key=settings.api_key, api_token=settings.api_token, + sid=settings.account_sid ), data={ 'From': from_number, 'To': to_number, @@ -91,4 +99,24 @@ def make_a_call(from_number, to_number, caller_id): return response.json() def get_exotel_settings(): - return frappe.get_single('Exotel Settings') \ No newline at end of file + return frappe.get_single('Exotel Settings') + +@frappe.whitelist(allow_guest=True) +def get_phone_numbers(): + numbers = 'some number' + whitelist_numbers(numbers, 'for number') + return numbers + +def whitelist_numbers(numbers, caller_id): + settings = get_exotel_settings() + query = 'https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/CustomerWhitelist'.format( + api_key=settings.api_key, + api_token=settings.api_token, + sid=settings.account_sid + ) + response = requests.post(query, data={ + 'VirtualNumber': caller_id, + 'Number': numbers, + }) + + return response \ No newline at end of file diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 2410684429..5693ff0677 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -43,9 +43,10 @@ class CallPopup { 'label': 'Submit', 'click': () => { const values = this.dialog.get_values(); + if (!values.call_summary) return frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', { 'docname': this.call_log.name, - 'summary': `${__('Call Summary')}: ${values.call_summary}`, + 'summary': values.call_summary, }).then(() => { this.dialog.set_value('call_summary', ''); }); @@ -62,6 +63,7 @@ class CallPopup { this.dialog.$body.addClass('call-popup'); this.dialog.set_secondary_action(() => { clearInterval(this.updater); + delete erpnext.call_popup; this.dialog.hide(); }); this.dialog.show(); @@ -173,7 +175,7 @@ class CallPopup { const issue_field = this.dialog.fields_dict["last_issue"]; issue_field.set_value(issue.subject); issue_field.$wrapper - .append(`View ${issue.name}`); + .append(`View all issues from ${issue.customer}`); } }); } From cf270845f2790eaa37adf6a579786522ed3cf194 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 5 Jun 2019 10:04:51 +0530 Subject: [PATCH 16/50] fix: Indicator toggler --- erpnext/public/js/call_popup/call_popup.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 5693ff0677..9ae9fd1eab 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -106,7 +106,8 @@ class CallPopup { } set_indicator(color, blink=false) { - this.dialog.header.find('.indicator').removeClass('hidden').toggleClass('blink', blink).addClass(color); + let classes = `indicator ${color} ${blink ? 'blink': ''}`; + this.dialog.header.find('.indicator').attr('class', classes); } set_call_status(call_status) { From bf1195e0b9fa2b99c95b87fe8768ea47ba3dd230 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 5 Jun 2019 12:23:30 +0530 Subject: [PATCH 17/50] feat: Introduce communication module --- erpnext/communication/__init__.py | 0 erpnext/communication/doctype/__init__.py | 0 erpnext/modules.txt | 3 ++- 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 erpnext/communication/__init__.py create mode 100644 erpnext/communication/doctype/__init__.py diff --git a/erpnext/communication/__init__.py b/erpnext/communication/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/communication/doctype/__init__.py b/erpnext/communication/doctype/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/modules.txt b/erpnext/modules.txt index 9ef8937ee5..316d6de20e 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -22,4 +22,5 @@ ERPNext Integrations Non Profit Hotels Hub Node -Quality Management \ No newline at end of file +Quality Management +Communication \ No newline at end of file From 0d64343ec02170c8cbb32d7b1a8a7ba1ccaa1503 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 5 Jun 2019 12:25:17 +0530 Subject: [PATCH 18/50] feat: Add Call Log, Communication Medium, Communication Medium Timeslot doctype --- .../doctype/call_log/__init__.py | 0 .../doctype/call_log/call_log.js | 8 ++ .../doctype/call_log/call_log.json | 86 +++++++++++++++++++ .../doctype/call_log/call_log.py | 10 +++ .../doctype/call_log/test_call_log.py | 10 +++ .../doctype/communication_medium/__init__.py | 0 .../communication_medium.js | 8 ++ .../communication_medium.json | 81 +++++++++++++++++ .../communication_medium.py | 10 +++ .../test_communication_medium.py | 10 +++ .../communication_medium_timeslot/__init__.py | 0 .../communication_medium_timeslot.json | 56 ++++++++++++ .../communication_medium_timeslot.py | 10 +++ 13 files changed, 289 insertions(+) create mode 100644 erpnext/communication/doctype/call_log/__init__.py create mode 100644 erpnext/communication/doctype/call_log/call_log.js create mode 100644 erpnext/communication/doctype/call_log/call_log.json create mode 100644 erpnext/communication/doctype/call_log/call_log.py create mode 100644 erpnext/communication/doctype/call_log/test_call_log.py create mode 100644 erpnext/communication/doctype/communication_medium/__init__.py create mode 100644 erpnext/communication/doctype/communication_medium/communication_medium.js create mode 100644 erpnext/communication/doctype/communication_medium/communication_medium.json create mode 100644 erpnext/communication/doctype/communication_medium/communication_medium.py create mode 100644 erpnext/communication/doctype/communication_medium/test_communication_medium.py create mode 100644 erpnext/communication/doctype/communication_medium_timeslot/__init__.py create mode 100644 erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.json create mode 100644 erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.py diff --git a/erpnext/communication/doctype/call_log/__init__.py b/erpnext/communication/doctype/call_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/communication/doctype/call_log/call_log.js b/erpnext/communication/doctype/call_log/call_log.js new file mode 100644 index 0000000000..0018516ec0 --- /dev/null +++ b/erpnext/communication/doctype/call_log/call_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Call Log', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json new file mode 100644 index 0000000000..0da5970137 --- /dev/null +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -0,0 +1,86 @@ +{ + "autoname": "call_id", + "creation": "2019-06-05 12:07:02.634534", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "call_id", + "call_from", + "column_break_3", + "received_by", + "section_break_5", + "call_status", + "call_duration", + "call_summary" + ], + "fields": [ + { + "fieldname": "call_id", + "fieldtype": "Data", + "label": "Call ID", + "read_only": 1 + }, + { + "fieldname": "call_from", + "fieldtype": "Data", + "label": "Call From", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "received_by", + "fieldtype": "Data", + "label": "Received By", + "read_only": 1 + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "call_status", + "fieldtype": "Select", + "label": "Call Status", + "options": "Ringing\nIn Progress\nMissed", + "read_only": 1 + }, + { + "description": "Call Duration in seconds", + "fieldname": "call_duration", + "fieldtype": "Int", + "label": "Call Duration", + "read_only": 1 + }, + { + "fieldname": "call_summary", + "fieldtype": "Data", + "label": "Call Summary", + "read_only": 1 + } + ], + "modified": "2019-06-05 12:08:55.527178", + "modified_by": "Administrator", + "module": "Communication", + "name": "Call Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/communication/doctype/call_log/call_log.py new file mode 100644 index 0000000000..fcca0e4b49 --- /dev/null +++ b/erpnext/communication/doctype/call_log/call_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class CallLog(Document): + pass diff --git a/erpnext/communication/doctype/call_log/test_call_log.py b/erpnext/communication/doctype/call_log/test_call_log.py new file mode 100644 index 0000000000..dcc982c000 --- /dev/null +++ b/erpnext/communication/doctype/call_log/test_call_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestCallLog(unittest.TestCase): + pass diff --git a/erpnext/communication/doctype/communication_medium/__init__.py b/erpnext/communication/doctype/communication_medium/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.js b/erpnext/communication/doctype/communication_medium/communication_medium.js new file mode 100644 index 0000000000..e37cd5b454 --- /dev/null +++ b/erpnext/communication/doctype/communication_medium/communication_medium.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Communication Medium', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.json b/erpnext/communication/doctype/communication_medium/communication_medium.json new file mode 100644 index 0000000000..f009b38877 --- /dev/null +++ b/erpnext/communication/doctype/communication_medium/communication_medium.json @@ -0,0 +1,81 @@ +{ + "autoname": "Prompt", + "creation": "2019-06-05 11:48:30.572795", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "communication_medium_type", + "catch_all", + "column_break_3", + "provider", + "disabled", + "timeslots_section", + "timeslots" + ], + "fields": [ + { + "fieldname": "communication_medium_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Communication Medium Type", + "options": "Voice\nEmail\nChat", + "reqd": 1 + }, + { + "description": "If there is no assigned timeslot, then communication will be handled by this group", + "fieldname": "catch_all", + "fieldtype": "Link", + "label": "Catch All", + "options": "Employee Group" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "provider", + "fieldtype": "Link", + "label": "Provider", + "options": "Supplier" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "timeslots_section", + "fieldtype": "Section Break", + "label": "Timeslots" + }, + { + "fieldname": "timeslots", + "fieldtype": "Table", + "label": "Timeslots", + "options": "Communication Medium Timeslot" + } + ], + "modified": "2019-06-05 11:49:30.769006", + "modified_by": "Administrator", + "module": "Communication", + "name": "Communication Medium", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.py b/erpnext/communication/doctype/communication_medium/communication_medium.py new file mode 100644 index 0000000000..f233da07d5 --- /dev/null +++ b/erpnext/communication/doctype/communication_medium/communication_medium.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class CommunicationMedium(Document): + pass diff --git a/erpnext/communication/doctype/communication_medium/test_communication_medium.py b/erpnext/communication/doctype/communication_medium/test_communication_medium.py new file mode 100644 index 0000000000..fc5754fe98 --- /dev/null +++ b/erpnext/communication/doctype/communication_medium/test_communication_medium.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestCommunicationMedium(unittest.TestCase): + pass diff --git a/erpnext/communication/doctype/communication_medium_timeslot/__init__.py b/erpnext/communication/doctype/communication_medium_timeslot/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.json b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.json new file mode 100644 index 0000000000..b278ca08f5 --- /dev/null +++ b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.json @@ -0,0 +1,56 @@ +{ + "creation": "2019-06-05 11:43:38.897272", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "day_of_week", + "from_time", + "to_time", + "employee_group" + ], + "fields": [ + { + "fieldname": "day_of_week", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Day of Week", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "reqd": 1 + }, + { + "columns": 2, + "fieldname": "from_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "From Time", + "reqd": 1 + }, + { + "columns": 2, + "fieldname": "to_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "To Time", + "reqd": 1 + }, + { + "fieldname": "employee_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee Group", + "options": "Employee Group", + "reqd": 1 + } + ], + "istable": 1, + "modified": "2019-06-05 12:19:59.994979", + "modified_by": "Administrator", + "module": "Communication", + "name": "Communication Medium Timeslot", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.py b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.py new file mode 100644 index 0000000000..d68d2d67a7 --- /dev/null +++ b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class CommunicationMediumTimeslot(Document): + pass From 27234ff907c109b05d34911cf1bafe5e294dc689 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 6 Jun 2019 10:57:13 +0530 Subject: [PATCH 19/50] fix: Update call log doctype --- .../communication/doctype/call_log/call_log.json | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json index 0da5970137..fe87ae9737 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -1,5 +1,5 @@ { - "autoname": "call_id", + "autoname": "field:call_id", "creation": "2019-06-05 12:07:02.634534", "doctype": "DocType", "engine": "InnoDB", @@ -18,11 +18,13 @@ "fieldname": "call_id", "fieldtype": "Data", "label": "Call ID", - "read_only": 1 + "read_only": 1, + "unique": 1 }, { "fieldname": "call_from", "fieldtype": "Data", + "in_list_view": 1, "label": "Call From", "read_only": 1 }, @@ -43,14 +45,16 @@ { "fieldname": "call_status", "fieldtype": "Select", + "in_list_view": 1, "label": "Call Status", - "options": "Ringing\nIn Progress\nMissed", + "options": "Ringing\nIn Progress\nCompleted\nMissed", "read_only": 1 }, { "description": "Call Duration in seconds", "fieldname": "call_duration", "fieldtype": "Int", + "in_list_view": 1, "label": "Call Duration", "read_only": 1 }, @@ -61,7 +65,7 @@ "read_only": 1 } ], - "modified": "2019-06-05 12:08:55.527178", + "modified": "2019-06-06 07:41:26.208109", "modified_by": "Administrator", "module": "Communication", "name": "Call Log", @@ -82,5 +86,6 @@ ], "sort_field": "modified", "sort_order": "ASC", + "title_field": "call_from", "track_changes": 1 } \ No newline at end of file From 44c0e9d54901c80b4fe780453e6dc4a7eac24eff Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 6 Jun 2019 11:18:16 +0530 Subject: [PATCH 20/50] fix: Create call log instead of communication --- erpnext/crm/doctype/utils.py | 11 ++++---- .../exotel_integration.py | 27 +++++++------------ 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 26cb2981d8..93d440cccb 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -70,12 +70,13 @@ def get_last_interaction(number, reference_doc): @frappe.whitelist() def add_call_summary(docname, summary): - communication = frappe.get_doc('Communication', docname) + call_log = frappe.get_doc('Call Log', docname) content = _('Call Summary by {0}: {1}').format( frappe.utils.get_fullname(frappe.session.user), summary) - if not communication.content: - communication.content = content + if not call_log.call_summary: + call_log.call_summary = content else: - communication.content += '\n' + content - communication.save(ignore_permissions=True) + call_log.call_summary += '
' + content + call_log.save(ignore_permissions=True) + diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index c70b0948ae..358eb3be0c 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -47,29 +47,20 @@ def close_call_log(call_payload): def get_call_log(call_payload, create_new_if_not_found=True): - communication = frappe.get_all('Communication', { - 'communication_medium': 'Phone', + call_log = frappe.get_all('Call Log', { 'call_id': call_payload.get('CallSid'), }, limit=1) - if communication: - communication = frappe.get_doc('Communication', communication[0].name) - return communication + if call_log: + return frappe.get_doc('Call Log', call_log[0].name) elif create_new_if_not_found: - communication = frappe.new_doc('Communication') - communication.subject = frappe._('Call from {}').format(call_payload.get("CallFrom")) - communication.communication_medium = 'Phone' - communication.phone_no = call_payload.get("CallFrom") - communication.comment_type = 'Info' - communication.communication_type = 'Communication' - communication.sent_or_received = 'Received' - communication.communication_date = call_payload.get('StartTime') - communication.call_id = call_payload.get('CallSid') - communication.status = 'Open' - communication.content = frappe._('Call from {}').format(call_payload.get("CallFrom")) - communication.save(ignore_permissions=True) + call_log = frappe.new_doc('Call Log') + call_log.call_id = call_payload.get('CallSid') + call_log.call_from = call_payload.get('CallFrom') + call_log.status = 'Ringing' + call_log.save(ignore_permissions=True) frappe.db.commit() - return communication + return call_log @frappe.whitelist() def get_call_status(call_id): From 87645e98ad3a562ee0ccbb5245dc44d03ae6a7b8 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 6 Jun 2019 11:24:31 +0530 Subject: [PATCH 21/50] fix: Get available employees from communication medium --- erpnext/crm/doctype/utils.py | 14 +++++++++ .../exotel_integration.py | 29 ++++++++----------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 93d440cccb..93ad0932eb 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -79,4 +79,18 @@ def add_call_summary(docname, summary): call_log.call_summary += '
' + content call_log.save(ignore_permissions=True) +def get_employee_emails_for_popup(): + employee_emails = [] + now_time = frappe.utils.nowtime() + weekday = frappe.utils.get_weekday() + available_employee_groups = frappe.db.sql("""SELECT `parent`, `employee_group` + FROM `tabCommunication Medium Timeslot` + WHERE `day_of_week` = %s AND + %s BETWEEN `from_time` AND `to_time` + """, (weekday, now_time), as_dict=1) + + for group in available_employee_groups: + employee_emails += [e.user_id for e in frappe.get_doc('Employee Group', group.employee_group).employee_list] + + return employee_emails diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 358eb3be0c..41f8c26252 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -1,5 +1,5 @@ import frappe -from erpnext.crm.doctype.utils import get_document_with_phone_number +from erpnext.crm.doctype.utils import get_document_with_phone_number, get_employee_emails_for_popup import requests # api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call @@ -9,39 +9,34 @@ def handle_incoming_call(*args, **kwargs): exotel_settings = get_exotel_settings() if not exotel_settings.enabled: return - employee_email = kwargs.get('AgentEmail') status = kwargs.get('Status') if status == 'free': # call disconnected for agent # "and get_call_status(kwargs.get('CallSid')) in ['in-progress']" - additional check to ensure if the call was redirected - frappe.publish_realtime('call_disconnected', user=employee_email) return call_log = get_call_log(kwargs) - data = frappe._dict({ - 'call_from': kwargs.get('CallFrom'), - 'agent_email': kwargs.get('AgentEmail'), - 'call_type': kwargs.get('Direction'), - 'call_log': call_log, - 'call_status_method': 'erpnext.erpnext_integrations.exotel_integration.get_call_status' - }) - - frappe.publish_realtime('show_call_popup', data, user=data.agent_email) + employee_emails = get_employee_emails_for_popup() + for email in employee_emails: + frappe.publish_realtime('show_call_popup', call_log, user=email) @frappe.whitelist(allow_guest=True) def handle_end_call(*args, **kwargs): - close_call_log(kwargs) + frappe.publish_realtime('call_disconnected', data=kwargs.get('CallSid')) + update_call_log(kwargs, 'Completed') @frappe.whitelist(allow_guest=True) def handle_missed_call(*args, **kwargs): - close_call_log(kwargs) + frappe.publish_realtime('call_disconnected', data=kwargs.get('CallSid')) + update_call_log(kwargs, 'Missed') -def close_call_log(call_payload): - call_log = get_call_log(call_payload) +def update_call_log(call_payload, status): + call_log = get_call_log(call_payload, False) if call_log: - call_log.status = 'Closed' + call_log.call_status = status + call_log.call_duration = call_payload.get('DialCallDuration') or 0 call_log.save(ignore_permissions=True) frappe.db.commit() From c0a640c4626d117052388dd40163576606d7264b Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 6 Jun 2019 14:47:40 +0530 Subject: [PATCH 22/50] feat: Add user_id of employee to employee group --- .../employee_group_table.json | 142 +++++------------- 1 file changed, 39 insertions(+), 103 deletions(-) diff --git a/erpnext/hr/doctype/employee_group_table/employee_group_table.json b/erpnext/hr/doctype/employee_group_table/employee_group_table.json index f2e77700b8..4e0045cdeb 100644 --- a/erpnext/hr/doctype/employee_group_table/employee_group_table.json +++ b/erpnext/hr/doctype/employee_group_table/employee_group_table.json @@ -1,109 +1,45 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-11-19 12:39:46.153061", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "creation": "2018-11-19 12:39:46.153061", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "user_id" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.first_name", - "fieldname": "employee_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fetch_from": "employee.first_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Employee Name" + }, + { + "fetch_from": "employee.user_id", + "fieldname": "user_id", + "fieldtype": "Data", + "label": "ERPNext User ID", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-11-19 13:18:17.281656", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Group Table", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "modified": "2019-06-06 10:41:20.313756", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Group Table", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file From 97780613ad49212d1e4b24ee65a4fe2998ed21be Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 6 Jun 2019 14:48:37 +0530 Subject: [PATCH 23/50] fix: Improve call popup UI --- erpnext/crm/doctype/utils.py | 15 +++-- .../exotel_integration.py | 4 +- erpnext/public/js/call_popup/call_popup.js | 62 +++++++------------ erpnext/public/less/call_popup.less | 8 --- 4 files changed, 32 insertions(+), 57 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 93ad0932eb..75562dd3b6 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -42,14 +42,13 @@ def get_last_interaction(number, reference_doc): if query_condition: query_condition = query_condition[:-2] - - last_communication = frappe.db.sql(""" - SELECT `name`, `content` - FROM `tabCommunication` - WHERE {} - ORDER BY `modified` - LIMIT 1 - """.format(query_condition)) + last_communication = frappe.db.sql(""" + SELECT `name`, `content` + FROM `tabCommunication` + WHERE {} + ORDER BY `modified` + LIMIT 1 + """.format(query_condition)) if customer_name: last_issue = frappe.get_all('Issue', { diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 41f8c26252..57cba78bdb 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -24,13 +24,13 @@ def handle_incoming_call(*args, **kwargs): @frappe.whitelist(allow_guest=True) def handle_end_call(*args, **kwargs): - frappe.publish_realtime('call_disconnected', data=kwargs.get('CallSid')) update_call_log(kwargs, 'Completed') + frappe.publish_realtime('call_disconnected', kwargs.get('CallSid')) @frappe.whitelist(allow_guest=True) def handle_missed_call(*args, **kwargs): - frappe.publish_realtime('call_disconnected', data=kwargs.get('CallSid')) update_call_log(kwargs, 'Missed') + frappe.publish_realtime('call_disconnected', kwargs.get('CallSid')) def update_call_log(call_payload, status): call_log = get_call_log(call_payload, False) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 9ae9fd1eab..c8c4e8b280 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -1,8 +1,7 @@ class CallPopup { - constructor({ call_from, call_log, call_status_method }) { - this.caller_number = call_from; + constructor(call_log) { + this.caller_number = call_log.call_from; this.call_log = call_log; - this.call_status_method = call_status_method; this.make(); } @@ -45,7 +44,7 @@ class CallPopup { const values = this.dialog.get_values(); if (!values.call_summary) return frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', { - 'docname': this.call_log.name, + 'docname': this.call_log.call_id, 'summary': values.call_summary, }).then(() => { this.dialog.set_value('call_summary', ''); @@ -56,10 +55,9 @@ class CallPopup { this.set_call_status(); } }); - this.set_call_status(this.call_log.call_status); + this.set_call_status(); this.make_caller_info_section(); this.dialog.get_close_btn().show(); - this.setup_call_status_updater(); this.dialog.$body.addClass('call-popup'); this.dialog.set_secondary_action(() => { clearInterval(this.updater); @@ -81,7 +79,7 @@ class CallPopup { wrapper.append(`
Unknown Number: ${this.caller_number}
- + ${__('Create New Contact')}
@@ -89,12 +87,14 @@ class CallPopup { } else { const link = contact.links ? contact.links[0] : null; const contact_link = link ? frappe.utils.get_form_link(link.link_doctype, link.link_name, true): ''; + const contact_name = `${contact.first_name || ''} ${contact.last_name || ''}` wrapper.append(`
- -
- ${contact.first_name} ${contact.last_name} - ${contact.mobile_no} + ${frappe.avatar(null, 'avatar-xl', contact_name, contact.image)} +
+
${contact_name}
+
${contact.mobile_no || ''}
+
${contact.phone_no || ''}
${contact_link}
@@ -113,17 +113,17 @@ class CallPopup { set_call_status(call_status) { let title = ''; call_status = call_status || this.call_log.call_status; - if (['busy', 'completed'].includes(call_status) || !call_status) { + if (['Ringing'].includes(call_status) || !call_status) { title = __('Incoming call from {0}', - [this.contact ? `${this.contact.first_name} ${this.contact.last_name}` : this.caller_number]); + [this.contact ? `${this.contact.first_name || ''} ${this.contact.last_name || ''}` : this.caller_number]); this.set_indicator('blue', true); - } else if (call_status === 'in-progress') { + } else if (call_status === 'In Progress') { title = __('Call Connected'); this.set_indicator('yellow'); } else if (call_status === 'missed') { this.set_indicator('red'); title = __('Call Missed'); - } else if (call_status === 'disconnected') { + } else if (['Completed', 'Disconnected'].includes(call_status)) { this.set_indicator('red'); title = __('Call Disconnected'); } else { @@ -133,27 +133,12 @@ class CallPopup { this.dialog.set_title(title); } - update(data) { - this.call_log = data.call_log; + update_call_log(call_log) { + this.call_log = call_log; this.set_call_status(); } - - setup_call_status_updater() { - this.updater = setInterval(this.get_call_status.bind(this), 20000); - } - - get_call_status() { - frappe.xcall(this.call_status_method, { - 'call_id': this.call_log.call_id - }).then((call_status) => { - if (call_status === 'completed') { - clearInterval(this.updater); - } - }); - } - disconnect_call() { - this.set_call_status('disconnected'); + this.set_call_status('Disconnected'); clearInterval(this.updater); } @@ -183,17 +168,16 @@ class CallPopup { } $(document).on('app_ready', function () { - frappe.realtime.on('show_call_popup', data => { + frappe.realtime.on('show_call_popup', call_log => { if (!erpnext.call_popup) { - erpnext.call_popup = new CallPopup(data); + erpnext.call_popup = new CallPopup(call_log); } else { - erpnext.call_popup.update(data); + erpnext.call_popup.update_call_log(call_log); erpnext.call_popup.dialog.show(); } }); - - frappe.realtime.on('call_disconnected', () => { - if (erpnext.call_popup) { + frappe.realtime.on('call_disconnected', id => { + if (erpnext.call_popup && erpnext.call_popup.call_log.call_id === id) { erpnext.call_popup.disconnect_call(); } }); diff --git a/erpnext/public/less/call_popup.less b/erpnext/public/less/call_popup.less index 3f4ffef3c0..4b2ddfa9b4 100644 --- a/erpnext/public/less/call_popup.less +++ b/erpnext/public/less/call_popup.less @@ -1,12 +1,4 @@ .call-popup { - .caller-info { - padding: 0 15px; - } - img { - width: auto; - height: 100px; - margin-right: 15px; - } a:hover { text-decoration: underline; } From c8c17422f7ac78467bf6a02fc880380ee37dce05 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 7 Jun 2019 10:22:50 +0530 Subject: [PATCH 24/50] fix: Change call log fieldname and some cleanups --- .../doctype/call_log/call_log.json | 70 +++++++++---------- erpnext/crm/doctype/utils.py | 10 +-- .../exotel_integration.py | 24 ++++--- erpnext/public/js/call_popup/call_popup.js | 36 ++++------ 4 files changed, 69 insertions(+), 71 deletions(-) diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json index fe87ae9737..bb428757e5 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -1,71 +1,71 @@ { - "autoname": "field:call_id", + "autoname": "field:id", "creation": "2019-06-05 12:07:02.634534", "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "call_id", - "call_from", + "id", + "from", "column_break_3", - "received_by", + "to", "section_break_5", - "call_status", - "call_duration", - "call_summary" + "status", + "duration", + "summary" ], "fields": [ - { - "fieldname": "call_id", - "fieldtype": "Data", - "label": "Call ID", - "read_only": 1, - "unique": 1 - }, - { - "fieldname": "call_from", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Call From", - "read_only": 1 - }, { "fieldname": "column_break_3", "fieldtype": "Column Break" }, - { - "fieldname": "received_by", - "fieldtype": "Data", - "label": "Received By", - "read_only": 1 - }, { "fieldname": "section_break_5", "fieldtype": "Section Break" }, { - "fieldname": "call_status", + "fieldname": "id", + "fieldtype": "Data", + "label": "ID", + "read_only": 1, + "unique": 1 + }, + { + "fieldname": "from", + "fieldtype": "Data", + "in_list_view": 1, + "label": "From", + "read_only": 1 + }, + { + "fieldname": "to", + "fieldtype": "Data", + "label": "To", + "read_only": 1 + }, + { + "fieldname": "status", "fieldtype": "Select", "in_list_view": 1, - "label": "Call Status", + "label": "Status", "options": "Ringing\nIn Progress\nCompleted\nMissed", "read_only": 1 }, { "description": "Call Duration in seconds", - "fieldname": "call_duration", + "fieldname": "duration", "fieldtype": "Int", "in_list_view": 1, - "label": "Call Duration", + "label": "Duration", "read_only": 1 }, { - "fieldname": "call_summary", + "fieldname": "summary", "fieldtype": "Data", - "label": "Call Summary", + "label": "Summary", "read_only": 1 } ], - "modified": "2019-06-06 07:41:26.208109", + "modified": "2019-06-07 09:49:07.623814", "modified_by": "Administrator", "module": "Communication", "name": "Call Log", @@ -86,6 +86,6 @@ ], "sort_field": "modified", "sort_order": "ASC", - "title_field": "call_from", + "title_field": "from", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 75562dd3b6..5781e39634 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -78,17 +78,17 @@ def add_call_summary(docname, summary): call_log.call_summary += '
' + content call_log.save(ignore_permissions=True) -def get_employee_emails_for_popup(): +def get_employee_emails_for_popup(communication_medium): employee_emails = [] now_time = frappe.utils.nowtime() weekday = frappe.utils.get_weekday() available_employee_groups = frappe.db.sql("""SELECT `parent`, `employee_group` FROM `tabCommunication Medium Timeslot` - WHERE `day_of_week` = %s AND - %s BETWEEN `from_time` AND `to_time` - """, (weekday, now_time), as_dict=1) - + WHERE `day_of_week` = %s + AND `parent` = %s + AND %s BETWEEN `from_time` AND `to_time` + """, (weekday, communication_medium, now_time), as_dict=1) for group in available_employee_groups: employee_emails += [e.user_id for e in frappe.get_doc('Employee Group', group.employee_group).employee_list] diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 57cba78bdb..bace40ff62 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -3,6 +3,8 @@ from erpnext.crm.doctype.utils import get_document_with_phone_number, get_employ import requests # api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call +# api/method/erpnext.erpnext_integrations.exotel_integration.handle_end_call +# api/method/erpnext.erpnext_integrations.exotel_integration.handle_missed_call @frappe.whitelist(allow_guest=True) def handle_incoming_call(*args, **kwargs): @@ -18,41 +20,43 @@ def handle_incoming_call(*args, **kwargs): call_log = get_call_log(kwargs) - employee_emails = get_employee_emails_for_popup() + employee_emails = get_employee_emails_for_popup(kwargs.get('To')) for email in employee_emails: frappe.publish_realtime('show_call_popup', call_log, user=email) @frappe.whitelist(allow_guest=True) def handle_end_call(*args, **kwargs): - update_call_log(kwargs, 'Completed') - frappe.publish_realtime('call_disconnected', kwargs.get('CallSid')) + call_log = update_call_log(kwargs, 'Completed') + frappe.publish_realtime('call_disconnected', call_log) @frappe.whitelist(allow_guest=True) def handle_missed_call(*args, **kwargs): - update_call_log(kwargs, 'Missed') - frappe.publish_realtime('call_disconnected', kwargs.get('CallSid')) + call_log = update_call_log(kwargs, 'Missed') + frappe.publish_realtime('call_disconnected', call_log) def update_call_log(call_payload, status): call_log = get_call_log(call_payload, False) if call_log: - call_log.call_status = status - call_log.call_duration = call_payload.get('DialCallDuration') or 0 + call_log.status = status + call_log.duration = call_payload.get('DialCallDuration') or 0 call_log.save(ignore_permissions=True) frappe.db.commit() + return call_log def get_call_log(call_payload, create_new_if_not_found=True): call_log = frappe.get_all('Call Log', { - 'call_id': call_payload.get('CallSid'), + 'id': call_payload.get('CallSid'), }, limit=1) if call_log: return frappe.get_doc('Call Log', call_log[0].name) elif create_new_if_not_found: call_log = frappe.new_doc('Call Log') - call_log.call_id = call_payload.get('CallSid') - call_log.call_from = call_payload.get('CallFrom') + call_log.id = call_payload.get('CallSid') + call_log.to = call_payload.get('To') call_log.status = 'Ringing' + setattr(call_log, 'from', call_payload.get('CallFrom')) call_log.save(ignore_permissions=True) frappe.db.commit() return call_log diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index c8c4e8b280..501299c0a8 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -1,6 +1,6 @@ class CallPopup { constructor(call_log) { - this.caller_number = call_log.call_from; + this.caller_number = call_log.from; this.call_log = call_log; this.make(); } @@ -20,17 +20,11 @@ class CallPopup { 'label': "Last Communication", 'fieldname': 'last_communication', 'read_only': true - }, { - 'fieldname': 'last_communication_link', - 'fieldtype': 'HTML', }, { 'fieldtype': 'Small Text', 'label': "Last Issue", 'fieldname': 'last_issue', 'read_only': true - }, { - 'fieldname': 'last_issue_link', - 'fieldtype': 'HTML', }, { 'fieldtype': 'Column Break', }, { @@ -44,26 +38,23 @@ class CallPopup { const values = this.dialog.get_values(); if (!values.call_summary) return frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', { - 'docname': this.call_log.call_id, + 'docname': this.call_log.id, 'summary': values.call_summary, }).then(() => { this.dialog.set_value('call_summary', ''); }); } }], - on_minimize_toggle: () => { - this.set_call_status(); - } }); this.set_call_status(); this.make_caller_info_section(); this.dialog.get_close_btn().show(); this.dialog.$body.addClass('call-popup'); this.dialog.set_secondary_action(() => { - clearInterval(this.updater); delete erpnext.call_popup; this.dialog.hide(); }); + frappe.utils.play_sound("incoming_call"); this.dialog.show(); } @@ -112,7 +103,7 @@ class CallPopup { set_call_status(call_status) { let title = ''; - call_status = call_status || this.call_log.call_status; + call_status = call_status || this.call_log.status; if (['Ringing'].includes(call_status) || !call_status) { title = __('Incoming call from {0}', [this.contact ? `${this.contact.first_name || ''} ${this.contact.last_name || ''}` : this.caller_number]); @@ -120,7 +111,7 @@ class CallPopup { } else if (call_status === 'In Progress') { title = __('Call Connected'); this.set_indicator('yellow'); - } else if (call_status === 'missed') { + } else if (call_status === 'Missed') { this.set_indicator('red'); title = __('Call Missed'); } else if (['Completed', 'Disconnected'].includes(call_status)) { @@ -137,9 +128,10 @@ class CallPopup { this.call_log = call_log; this.set_call_status(); } - disconnect_call() { - this.set_call_status('Disconnected'); - clearInterval(this.updater); + + call_disconnected(call_log) { + frappe.utils.play_sound("call_disconnect"); + this.update_call_log(call_log); } make_last_interaction_section() { @@ -147,12 +139,14 @@ class CallPopup { 'number': this.caller_number, 'reference_doc': this.contact }).then(data => { + const comm_field = this.dialog.fields_dict["last_communication"]; if (data.last_communication) { const comm = data.last_communication; // this.dialog.set_df_property('last_interaction', 'hidden', false); - const comm_field = this.dialog.fields_dict["last_communication"]; comm_field.set_value(comm.content); comm_field.$wrapper.append(frappe.utils.get_form_link('Communication', comm.name)); + } else { + comm_field.$wrapper.hide(); } if (data.last_issue) { @@ -176,9 +170,9 @@ $(document).on('app_ready', function () { erpnext.call_popup.dialog.show(); } }); - frappe.realtime.on('call_disconnected', id => { - if (erpnext.call_popup && erpnext.call_popup.call_log.call_id === id) { - erpnext.call_popup.disconnect_call(); + frappe.realtime.on('call_disconnected', call_log => { + if (erpnext.call_popup && erpnext.call_popup.call_log.id === call_log.id) { + erpnext.call_popup.call_disconnected(call_log); } }); }); From 6a87e3338b5b18413fdde207ca25a49db53f8f05 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 7 Jun 2019 11:52:39 +0530 Subject: [PATCH 25/50] fix: Call popup UI --- erpnext/public/js/call_popup/call_popup.js | 20 +++++++++++--------- erpnext/public/less/call_popup.less | 4 ++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 501299c0a8..6510a0f862 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -19,12 +19,14 @@ class CallPopup { 'fieldtype': 'Small Text', 'label': "Last Communication", 'fieldname': 'last_communication', - 'read_only': true + 'read_only': true, + 'default': `${__('No communication found.')}` }, { 'fieldtype': 'Small Text', 'label': "Last Issue", 'fieldname': 'last_issue', - 'read_only': true + 'read_only': true, + 'default': `${__('No issue raised by the customer.')}` }, { 'fieldtype': 'Column Break', }, { @@ -76,12 +78,12 @@ class CallPopup {
`); } else { + const contact_name = frappe.utils.get_form_link('Contact', contact.name, true); const link = contact.links ? contact.links[0] : null; - const contact_link = link ? frappe.utils.get_form_link(link.link_doctype, link.link_name, true): ''; - const contact_name = `${contact.first_name || ''} ${contact.last_name || ''}` + let contact_link = link ? frappe.utils.get_form_link(link.link_doctype, link.link_name, true): ''; wrapper.append(`
- ${frappe.avatar(null, 'avatar-xl', contact_name, contact.image)} + ${frappe.avatar(null, 'avatar-xl', contact.name, contact.image)}
${contact_name}
${contact.mobile_no || ''}
@@ -132,6 +134,7 @@ class CallPopup { call_disconnected(call_log) { frappe.utils.play_sound("call_disconnect"); this.update_call_log(call_log); + setTimeout(this.get_close_btn().click, 10000); } make_last_interaction_section() { @@ -145,8 +148,6 @@ class CallPopup { // this.dialog.set_df_property('last_interaction', 'hidden', false); comm_field.set_value(comm.content); comm_field.$wrapper.append(frappe.utils.get_form_link('Communication', comm.name)); - } else { - comm_field.$wrapper.hide(); } if (data.last_issue) { @@ -154,8 +155,9 @@ class CallPopup { // this.dialog.set_df_property('last_interaction', 'hidden', false); const issue_field = this.dialog.fields_dict["last_issue"]; issue_field.set_value(issue.subject); - issue_field.$wrapper - .append(`View all issues from ${issue.customer}`); + issue_field.$wrapper.append(` + View all issues from ${issue.customer} + `); } }); } diff --git a/erpnext/public/less/call_popup.less b/erpnext/public/less/call_popup.less index 4b2ddfa9b4..32e85ce16d 100644 --- a/erpnext/public/less/call_popup.less +++ b/erpnext/public/less/call_popup.less @@ -2,4 +2,8 @@ a:hover { text-decoration: underline; } + .for-description { + max-height: 250px; + overflow: scroll; + } } \ No newline at end of file From f1ffdb3c12c679dec923c57f67bc8aa23e753196 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 7 Jun 2019 12:48:13 +0530 Subject: [PATCH 26/50] feat: Add call sound effects --- erpnext/hooks.py | 5 +++++ erpnext/public/js/call_popup/call_popup.js | 4 ++-- erpnext/public/sounds/call-disconnect.mp3 | Bin 0 -> 18324 bytes erpnext/public/sounds/incoming-call.mp3 | Bin 0 -> 59075 bytes 4 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 erpnext/public/sounds/call-disconnect.mp3 create mode 100644 erpnext/public/sounds/incoming-call.mp3 diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 53da013f8d..34b7d2a2f6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -170,6 +170,11 @@ default_roles = [ {'role': 'Student', 'doctype':'Student', 'email_field': 'student_email_id'}, ] +sounds = [ + {"name": "incoming-call", "src": "/assets/erpnext/sounds/incoming-call.mp3", "volume": 0.2}, + {"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2}, +] + has_website_permission = { "Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission", "Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission", diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 6510a0f862..d3c9c0ce41 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -56,7 +56,7 @@ class CallPopup { delete erpnext.call_popup; this.dialog.hide(); }); - frappe.utils.play_sound("incoming_call"); + frappe.utils.play_sound("incoming-call"); this.dialog.show(); } @@ -132,7 +132,7 @@ class CallPopup { } call_disconnected(call_log) { - frappe.utils.play_sound("call_disconnect"); + frappe.utils.play_sound("call-disconnect"); this.update_call_log(call_log); setTimeout(this.get_close_btn().click, 10000); } diff --git a/erpnext/public/sounds/call-disconnect.mp3 b/erpnext/public/sounds/call-disconnect.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..1202273dfe53da883e0ef51fffeeb4b0ed087947 GIT binary patch literal 18324 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 B;CKK4 literal 0 HcmV?d00001 diff --git a/erpnext/public/sounds/incoming-call.mp3 b/erpnext/public/sounds/incoming-call.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..60431e30884144b60a74c882600c4fa43e36eea0 GIT binary patch literal 59075 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 z Date: Mon, 10 Jun 2019 09:52:53 +0530 Subject: [PATCH 27/50] fix: Codacy --- erpnext/crm/doctype/utils.py | 2 +- erpnext/public/js/call_popup/call_popup.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 5781e39634..acb074a16a 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -48,7 +48,7 @@ def get_last_interaction(number, reference_doc): WHERE {} ORDER BY `modified` LIMIT 1 - """.format(query_condition)) + """.format(query_condition)) # nosec if customer_name: last_issue = frappe.get_all('Issue', { diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index d3c9c0ce41..a15c37c85d 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -38,7 +38,7 @@ class CallPopup { 'label': 'Submit', 'click': () => { const values = this.dialog.get_values(); - if (!values.call_summary) return + if (!values.call_summary) return; frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', { 'docname': this.call_log.id, 'summary': values.call_summary, From 77302b39286f7666609c0bb0a660daefb9aca673 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 11 Jun 2019 08:01:18 +0530 Subject: [PATCH 28/50] fix: Save summary to right field --- erpnext/crm/doctype/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index acb074a16a..68b5d1fd18 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -70,12 +70,12 @@ def get_last_interaction(number, reference_doc): @frappe.whitelist() def add_call_summary(docname, summary): call_log = frappe.get_doc('Call Log', docname) - content = _('Call Summary by {0}: {1}').format( + summary = _('Call Summary by {0}: {1}').format( frappe.utils.get_fullname(frappe.session.user), summary) - if not call_log.call_summary: - call_log.call_summary = content + if not call_log.summary: + call_log.summary = summary else: - call_log.call_summary += '
' + content + call_log.summary += '
' + summary call_log.save(ignore_permissions=True) def get_employee_emails_for_popup(communication_medium): From 6013375fbb4c9430b864205d998838e647143221 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 11 Jun 2019 08:03:55 +0530 Subject: [PATCH 29/50] fix: Improve call popup UX - Close modal after 10 secs if user is has not entered any call summary - Show alert once the summary was saved --- .pylintrc | 1 - erpnext/crm/doctype/lead/lead.js | 9 ++++++ erpnext/public/js/call_popup/call_popup.js | 34 +++++++++++++--------- 3 files changed, 30 insertions(+), 14 deletions(-) delete mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 4b2ea0a564..0000000000 --- a/.pylintrc +++ /dev/null @@ -1 +0,0 @@ -disable=access-member-before-definition \ No newline at end of file diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index 8c1ab2f38f..d2e907d162 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -33,6 +33,7 @@ erpnext.LeadController = frappe.ui.form.Controller.extend({ frappe.dynamic_link = { doc: doc, fieldname: 'name', doctype: 'Lead' } if(!doc.__islocal && doc.__onload && !doc.__onload.is_customer) { + this.frm.add_custom_button(__("Call"), this.call); this.frm.add_custom_button(__("Customer"), this.create_customer, __('Create')); this.frm.add_custom_button(__("Opportunity"), this.create_opportunity, __('Create')); this.frm.add_custom_button(__("Quotation"), this.make_quotation, __('Create')); @@ -52,6 +53,14 @@ erpnext.LeadController = frappe.ui.form.Controller.extend({ }) }, + call: () => { + frappe.xcall('erpnext.erpnext_integrations.exotel_integration.make_a_call', { + 'to_number': this.frm.doc.phone, + 'from_number': '', + 'caller_id': '09513886363' + }).then(console.log) + }, + create_opportunity: function () { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_opportunity", diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index a15c37c85d..4fd3a5539f 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -35,15 +35,19 @@ class CallPopup { 'fieldname': 'call_summary', }, { 'fieldtype': 'Button', - 'label': 'Submit', + 'label': 'Save', 'click': () => { - const values = this.dialog.get_values(); - if (!values.call_summary) return; + const call_summary = this.dialog.get_value('call_summary'); + if (!call_summary) return; frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', { 'docname': this.call_log.id, - 'summary': values.call_summary, + 'summary': call_summary, }).then(() => { - this.dialog.set_value('call_summary', ''); + this.close_modal(); + frappe.show_alert({ + message: `${__('Call Summary Saved')}
${__('View call log')}`, + indicator: 'green' + }); }); } }], @@ -52,10 +56,7 @@ class CallPopup { this.make_caller_info_section(); this.dialog.get_close_btn().show(); this.dialog.$body.addClass('call-popup'); - this.dialog.set_secondary_action(() => { - delete erpnext.call_popup; - this.dialog.hide(); - }); + this.dialog.set_secondary_action(this.close_modal); frappe.utils.play_sound("incoming-call"); this.dialog.show(); } @@ -71,7 +72,7 @@ class CallPopup { if (!contact) { wrapper.append(`
-
Unknown Number: ${this.caller_number}
+
${__('Unknown Number')}: ${this.caller_number}
${__('Create New Contact')} @@ -131,10 +132,19 @@ class CallPopup { this.set_call_status(); } + close_modal() { + delete erpnext.call_popup; + this.dialog.hide(); + } + call_disconnected(call_log) { frappe.utils.play_sound("call-disconnect"); this.update_call_log(call_log); - setTimeout(this.get_close_btn().click, 10000); + setTimeout(() => { + if (!this.dialog.get_value('call_summary')) { + this.close_modal(); + } + }, 10000); } make_last_interaction_section() { @@ -145,14 +155,12 @@ class CallPopup { const comm_field = this.dialog.fields_dict["last_communication"]; if (data.last_communication) { const comm = data.last_communication; - // this.dialog.set_df_property('last_interaction', 'hidden', false); comm_field.set_value(comm.content); comm_field.$wrapper.append(frappe.utils.get_form_link('Communication', comm.name)); } if (data.last_issue) { const issue = data.last_issue; - // this.dialog.set_df_property('last_interaction', 'hidden', false); const issue_field = this.dialog.fields_dict["last_issue"]; issue_field.set_value(issue.subject); issue_field.$wrapper.append(` From 28f7c3ca3fe8b9e848bf6fef14eac3aeaf046eaf Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 11 Jun 2019 12:21:30 +0530 Subject: [PATCH 30/50] fix: Add Make contact button --- erpnext/public/js/call_popup/call_popup.js | 67 ++++++++++++++-------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 4fd3a5539f..960f00515c 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -56,7 +56,7 @@ class CallPopup { this.make_caller_info_section(); this.dialog.get_close_btn().show(); this.dialog.$body.addClass('call-popup'); - this.dialog.set_secondary_action(this.close_modal); + this.dialog.set_secondary_action(this.close_modal.bind(this)); frappe.utils.play_sound("incoming-call"); this.dialog.show(); } @@ -70,35 +70,54 @@ class CallPopup { wrapper.empty(); const contact = this.contact = contact_doc; if (!contact) { - wrapper.append(` - - `); + this.setup_unknown_caller(wrapper); } else { - const contact_name = frappe.utils.get_form_link('Contact', contact.name, true); - const link = contact.links ? contact.links[0] : null; - let contact_link = link ? frappe.utils.get_form_link(link.link_doctype, link.link_name, true): ''; - wrapper.append(` -
- ${frappe.avatar(null, 'avatar-xl', contact.name, contact.image)} -
-
${contact_name}
-
${contact.mobile_no || ''}
-
${contact.phone_no || ''}
- ${contact_link} -
-
- `); + this.setup_known_caller(wrapper); this.set_call_status(); this.make_last_interaction_section(); } }); } + setup_unknown_caller(wrapper) { + wrapper.append(` +
+ ${__('Unknown Number')}: ${this.caller_number} + +
+ `).find('button').click( + () => frappe.set_route(`Form/Contact/New Contact?phone=${this.caller_number}`) + ); + } + + setup_known_caller(wrapper) { + const contact = this.contact; + const contact_name = frappe.utils.get_form_link('Contact', contact.name, true); + const links = contact.links ? contact.links : []; + + let contact_links = ''; + + links.forEach(link => { + contact_links += `
${link.link_doctype}: ${frappe.utils.get_form_link(link.link_doctype, link.link_name, true)}
`; + }); + wrapper.append(` +
+ ${frappe.avatar(null, 'avatar-xl', contact.name, contact.image)} +
+
${contact_name}
+
${contact.mobile_no || ''}
+
${contact.phone_no || ''}
+ ${contact_links} +
+
+ `); + } + set_indicator(color, blink=false) { let classes = `indicator ${color} ${blink ? 'blink': ''}`; this.dialog.header.find('.indicator').attr('class', classes); @@ -133,8 +152,8 @@ class CallPopup { } close_modal() { - delete erpnext.call_popup; this.dialog.hide(); + delete erpnext.call_popup; } call_disconnected(call_log) { From 7ff124db954e2462651b869323e9616900ae4c34 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 11 Jun 2019 14:21:48 +0530 Subject: [PATCH 31/50] fix: Show lead name when the lead calls --- erpnext/public/js/call_popup/call_popup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 960f00515c..f994b772a7 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -97,7 +97,7 @@ class CallPopup { setup_known_caller(wrapper) { const contact = this.contact; - const contact_name = frappe.utils.get_form_link('Contact', contact.name, true); + const contact_name = frappe.utils.get_form_link(contact.doctype, contact.name, true, contact.lead_name); const links = contact.links ? contact.links : []; let contact_links = ''; From 2f847796670a3863a95b51e2f7095e285082d7b4 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 12 Jun 2019 10:31:07 +0530 Subject: [PATCH 32/50] fix: Last Communication for lead - Remove unused code - Remove unused import --- erpnext/crm/doctype/lead/lead.js | 9 --------- erpnext/crm/doctype/utils.py | 2 +- erpnext/erpnext_integrations/exotel_integration.py | 2 +- erpnext/public/js/call_popup/call_popup.js | 3 +-- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index d2e907d162..8c1ab2f38f 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -33,7 +33,6 @@ erpnext.LeadController = frappe.ui.form.Controller.extend({ frappe.dynamic_link = { doc: doc, fieldname: 'name', doctype: 'Lead' } if(!doc.__islocal && doc.__onload && !doc.__onload.is_customer) { - this.frm.add_custom_button(__("Call"), this.call); this.frm.add_custom_button(__("Customer"), this.create_customer, __('Create')); this.frm.add_custom_button(__("Opportunity"), this.create_opportunity, __('Create')); this.frm.add_custom_button(__("Quotation"), this.make_quotation, __('Create')); @@ -53,14 +52,6 @@ erpnext.LeadController = frappe.ui.form.Controller.extend({ }) }, - call: () => { - frappe.xcall('erpnext.erpnext_integrations.exotel_integration.make_a_call', { - 'to_number': this.frm.doc.phone, - 'from_number': '', - 'caller_id': '09513886363' - }).then(console.log) - }, - create_opportunity: function () { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_opportunity", diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 68b5d1fd18..b424ac3878 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -56,7 +56,7 @@ def get_last_interaction(number, reference_doc): }, ['name', 'subject', 'customer'], limit=1) elif reference_doc.doctype == 'Lead': - last_communication = frappe.get_all('Communication', { + last_communication = frappe.get_all('Communication', filters={ 'reference_doctype': reference_doc.doctype, 'reference_name': reference_doc.name, 'sent_or_received': 'Received' diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index bace40ff62..4e88c5b03a 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -1,5 +1,5 @@ import frappe -from erpnext.crm.doctype.utils import get_document_with_phone_number, get_employee_emails_for_popup +from erpnext.crm.doctype.utils import get_employee_emails_for_popup import requests # api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index f994b772a7..4df2321d18 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -175,7 +175,6 @@ class CallPopup { if (data.last_communication) { const comm = data.last_communication; comm_field.set_value(comm.content); - comm_field.$wrapper.append(frappe.utils.get_form_link('Communication', comm.name)); } if (data.last_issue) { @@ -183,7 +182,7 @@ class CallPopup { const issue_field = this.dialog.fields_dict["last_issue"]; issue_field.set_value(issue.subject); issue_field.$wrapper.append(` - View all issues from ${issue.customer} + ${__('View all issues from {0}', [issue.customer])} `); } }); From 06f80345426f26ca65e789ffa1485407b5130234 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 13 Jun 2019 17:13:54 +0530 Subject: [PATCH 33/50] refactor: Remove redundant code --- .../exotel_integration.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 4e88c5b03a..a0e8295886 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -63,24 +63,15 @@ def get_call_log(call_payload, create_new_if_not_found=True): @frappe.whitelist() def get_call_status(call_id): - print(call_id) - settings = get_exotel_settings() - response = requests.get('https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/erpnext/Calls/{call_id}.json'.format( - api_key=settings.api_key, - api_token=settings.api_token, - call_id=call_id - )) + endpoint = get_exotel_endpoint('Calls/{call_id}.json'.format(call_id=call_id)) + response = requests.get(endpoint) status = response.json().get('Call', {}).get('Status') return status @frappe.whitelist() def make_a_call(from_number, to_number, caller_id): - settings = get_exotel_settings() - response = requests.post('https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/Calls/connect.json?details=true'.format( - api_key=settings.api_key, - api_token=settings.api_token, - sid=settings.account_sid - ), data={ + endpoint = get_exotel_endpoint('Calls/connect.json?details=true') + response = requests.post(endpoint, data={ 'From': from_number, 'To': to_number, 'CallerId': caller_id @@ -98,15 +89,24 @@ def get_phone_numbers(): return numbers def whitelist_numbers(numbers, caller_id): - settings = get_exotel_settings() - query = 'https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/CustomerWhitelist'.format( - api_key=settings.api_key, - api_token=settings.api_token, - sid=settings.account_sid - ) - response = requests.post(query, data={ + endpoint = get_exotel_endpoint('CustomerWhitelist') + response = requests.post(endpoint, data={ 'VirtualNumber': caller_id, 'Number': numbers, }) - return response \ No newline at end of file + return response + +def get_all_exophones(): + endpoint = get_exotel_endpoint('IncomingPhoneNumbers') + response = requests.post(endpoint) + return response + +def get_exotel_endpoint(action): + settings = get_exotel_settings() + return 'https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/{action}'.format( + api_key=settings.api_key, + api_token=settings.api_token, + sid=settings.account_sid, + action=action + ) \ No newline at end of file From be1dddd596f7136b376d83d1acfd7417e49d3aa0 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 17 Jun 2019 08:06:14 +0530 Subject: [PATCH 34/50] feat: Add call recording URL field to call log --- erpnext/communication/doctype/call_log/call_log.json | 9 ++++++++- erpnext/erpnext_integrations/exotel_integration.py | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json index bb428757e5..23c876d904 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -11,6 +11,7 @@ "section_break_5", "status", "duration", + "recording_url", "summary" ], "fields": [ @@ -63,9 +64,15 @@ "fieldtype": "Data", "label": "Summary", "read_only": 1 + }, + { + "fieldname": "recording_url", + "fieldtype": "Data", + "label": "Recording URL", + "read_only": 1 } ], - "modified": "2019-06-07 09:49:07.623814", + "modified": "2019-06-17 08:01:46.881008", "modified_by": "Administrator", "module": "Communication", "name": "Call Log", diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index a0e8295886..a22eb6073b 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -39,6 +39,7 @@ def update_call_log(call_payload, status): if call_log: call_log.status = status call_log.duration = call_payload.get('DialCallDuration') or 0 + call_log.recording_url = call_payload.get('RecordingUrl') call_log.save(ignore_permissions=True) frappe.db.commit() return call_log From 3fdeffff7adc833d8bc4d45bed32374c0251d87e Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 17 Jun 2019 08:27:00 +0530 Subject: [PATCH 35/50] feat: Add medium field in call log doctype --- erpnext/communication/doctype/call_log/call_log.json | 10 ++++++++-- erpnext/erpnext_integrations/exotel_integration.py | 6 ++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json index 23c876d904..3bc8dbaa0a 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -6,8 +6,9 @@ "field_order": [ "id", "from", - "column_break_3", "to", + "column_break_3", + "medium", "section_break_5", "status", "duration", @@ -70,9 +71,14 @@ "fieldtype": "Data", "label": "Recording URL", "read_only": 1 + }, + { + "fieldname": "medium", + "fieldtype": "Data", + "label": "Medium" } ], - "modified": "2019-06-17 08:01:46.881008", + "modified": "2019-06-17 08:21:20.665441", "modified_by": "Administrator", "module": "Communication", "name": "Call Log", diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index a22eb6073b..32602d6311 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -12,10 +12,7 @@ def handle_incoming_call(*args, **kwargs): if not exotel_settings.enabled: return status = kwargs.get('Status') - if status == 'free': - # call disconnected for agent - # "and get_call_status(kwargs.get('CallSid')) in ['in-progress']" - additional check to ensure if the call was redirected return call_log = get_call_log(kwargs) @@ -55,7 +52,8 @@ def get_call_log(call_payload, create_new_if_not_found=True): elif create_new_if_not_found: call_log = frappe.new_doc('Call Log') call_log.id = call_payload.get('CallSid') - call_log.to = call_payload.get('To') + call_log.to = call_payload.get('CallTo') + call_log.medium = call_payload.get('To') call_log.status = 'Ringing' setattr(call_log, 'from', call_payload.get('CallFrom')) call_log.save(ignore_permissions=True) From 5ea6a5e33ada5b5d1bd6b6d7c5328ed812ec7725 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 17 Jun 2019 08:46:38 +0530 Subject: [PATCH 36/50] fix: Decouple call popup from exotel --- erpnext/communication/doctype/call_log/call_log.py | 13 +++++++++++-- erpnext/erpnext_integrations/exotel_integration.py | 6 ------ erpnext/public/js/call_popup/call_popup.js | 13 ++++++++----- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/communication/doctype/call_log/call_log.py index fcca0e4b49..053470687b 100644 --- a/erpnext/communication/doctype/call_log/call_log.py +++ b/erpnext/communication/doctype/call_log/call_log.py @@ -3,8 +3,17 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document +from erpnext.crm.doctype.utils import get_employee_emails_for_popup class CallLog(Document): - pass + def after_insert(self): + employee_emails = get_employee_emails_for_popup(self.medium) + for email in employee_emails: + frappe.publish_realtime('show_call_popup', self, user=email) + + def on_update(self): + doc_before_save = self.get_doc_before_save() + if doc_before_save.status in ['Ringing'] and self.status in ['Missed', 'Completed']: + frappe.publish_realtime('call_{id}_disconnected'.format(id=self.id), self) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 32602d6311..f91f757673 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -1,5 +1,4 @@ import frappe -from erpnext.crm.doctype.utils import get_employee_emails_for_popup import requests # api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call @@ -17,14 +16,9 @@ def handle_incoming_call(*args, **kwargs): call_log = get_call_log(kwargs) - employee_emails = get_employee_emails_for_popup(kwargs.get('To')) - for email in employee_emails: - frappe.publish_realtime('show_call_popup', call_log, user=email) - @frappe.whitelist(allow_guest=True) def handle_end_call(*args, **kwargs): call_log = update_call_log(kwargs, 'Completed') - frappe.publish_realtime('call_disconnected', call_log) @frappe.whitelist(allow_guest=True) def handle_missed_call(*args, **kwargs): diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 4df2321d18..8748980f51 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -2,6 +2,7 @@ class CallPopup { constructor(call_log) { this.caller_number = call_log.from; this.call_log = call_log; + this.setup_listener(); this.make(); } @@ -187,6 +188,13 @@ class CallPopup { } }); } + setup_listener() { + frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => { + this.call_disconnected(call_log); + // Remove call disconnect listener after the call is disconnected + frappe.realtime.off(`call_${this.call_log.id}_disconnected`); + }); + } } $(document).on('app_ready', function () { @@ -198,9 +206,4 @@ $(document).on('app_ready', function () { erpnext.call_popup.dialog.show(); } }); - frappe.realtime.on('call_disconnected', call_log => { - if (erpnext.call_popup && erpnext.call_popup.call_log.id === call_log.id) { - erpnext.call_popup.call_disconnected(call_log); - } - }); }); From eb0ca67cedeb5706a8424d51458c98480cd8cc5e Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 17 Jun 2019 08:59:12 +0530 Subject: [PATCH 37/50] fix: Caller name in call popup --- erpnext/public/js/call_popup/call_popup.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 8748980f51..4117b6c00c 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -98,7 +98,7 @@ class CallPopup { setup_known_caller(wrapper) { const contact = this.contact; - const contact_name = frappe.utils.get_form_link(contact.doctype, contact.name, true, contact.lead_name); + const contact_name = frappe.utils.get_form_link(contact.doctype, contact.name, true, this.get_caller_name()); const links = contact.links ? contact.links : []; let contact_links = ''; @@ -128,8 +128,7 @@ class CallPopup { let title = ''; call_status = call_status || this.call_log.status; if (['Ringing'].includes(call_status) || !call_status) { - title = __('Incoming call from {0}', - [this.contact ? `${this.contact.first_name || ''} ${this.contact.last_name || ''}` : this.caller_number]); + title = __('Incoming call from {0}', [this.get_caller_name()]); this.set_indicator('blue', true); } else if (call_status === 'In Progress') { title = __('Call Connected'); @@ -188,6 +187,9 @@ class CallPopup { } }); } + get_caller_name() { + return this.contact ? this.contact.lead_name || this.contact.name || '' : this.caller_number; + } setup_listener() { frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => { this.call_disconnected(call_log); From a86a07ef37b3ccdbf4e10f0d6a2310705dfc87b8 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 17 Jun 2019 09:29:46 +0530 Subject: [PATCH 38/50] fix: Add additional check to ensure doc_before save exists --- erpnext/communication/doctype/call_log/call_log.json | 5 +++-- erpnext/communication/doctype/call_log/call_log.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json index 3bc8dbaa0a..c3d6d07fa5 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -75,10 +75,11 @@ { "fieldname": "medium", "fieldtype": "Data", - "label": "Medium" + "label": "Medium", + "read_only": 1 } ], - "modified": "2019-06-17 08:21:20.665441", + "modified": "2019-06-17 09:02:48.150383", "modified_by": "Administrator", "module": "Communication", "name": "Call Log", diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/communication/doctype/call_log/call_log.py index 053470687b..66f1064e58 100644 --- a/erpnext/communication/doctype/call_log/call_log.py +++ b/erpnext/communication/doctype/call_log/call_log.py @@ -15,5 +15,5 @@ class CallLog(Document): def on_update(self): doc_before_save = self.get_doc_before_save() - if doc_before_save.status in ['Ringing'] and self.status in ['Missed', 'Completed']: + if doc_before_save and doc_before_save.status in ['Ringing'] and self.status in ['Missed', 'Completed']: frappe.publish_realtime('call_{id}_disconnected'.format(id=self.id), self) From 340ccb6c967dca3aa3b704bf65e5f8c3075bc534 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 17 Jun 2019 10:16:38 +0530 Subject: [PATCH 39/50] fix: Remove unwanted code --- erpnext/erpnext_integrations/exotel_integration.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index f91f757673..10d533c8df 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -14,16 +14,15 @@ def handle_incoming_call(*args, **kwargs): if status == 'free': return - call_log = get_call_log(kwargs) + create_call_log(kwargs) @frappe.whitelist(allow_guest=True) def handle_end_call(*args, **kwargs): - call_log = update_call_log(kwargs, 'Completed') + update_call_log(kwargs, 'Completed') @frappe.whitelist(allow_guest=True) def handle_missed_call(*args, **kwargs): - call_log = update_call_log(kwargs, 'Missed') - frappe.publish_realtime('call_disconnected', call_log) + update_call_log(kwargs, 'Missed') def update_call_log(call_payload, status): call_log = get_call_log(call_payload, False) @@ -35,7 +34,6 @@ def update_call_log(call_payload, status): frappe.db.commit() return call_log - def get_call_log(call_payload, create_new_if_not_found=True): call_log = frappe.get_all('Call Log', { 'id': call_payload.get('CallSid'), @@ -54,6 +52,8 @@ def get_call_log(call_payload, create_new_if_not_found=True): frappe.db.commit() return call_log +create_call_log = get_call_log + @frappe.whitelist() def get_call_status(call_id): endpoint = get_exotel_endpoint('Calls/{call_id}.json'.format(call_id=call_id)) From 520c403a05bd2e5e9357b4d188ddc2c044bc6ce2 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 17 Jun 2019 13:02:49 +0530 Subject: [PATCH 40/50] Delete call_log.js --- erpnext/communication/doctype/call_log/call_log.js | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 erpnext/communication/doctype/call_log/call_log.js diff --git a/erpnext/communication/doctype/call_log/call_log.js b/erpnext/communication/doctype/call_log/call_log.js deleted file mode 100644 index 0018516ec0..0000000000 --- a/erpnext/communication/doctype/call_log/call_log.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Call Log', { - // refresh: function(frm) { - - // } -}); From 2e968ece1a879eaba980359e3366af890c685804 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 17 Jun 2019 13:03:44 +0530 Subject: [PATCH 41/50] Delete test_call_log.py --- .../communication/doctype/call_log/test_call_log.py | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 erpnext/communication/doctype/call_log/test_call_log.py diff --git a/erpnext/communication/doctype/call_log/test_call_log.py b/erpnext/communication/doctype/call_log/test_call_log.py deleted file mode 100644 index dcc982c000..0000000000 --- a/erpnext/communication/doctype/call_log/test_call_log.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestCallLog(unittest.TestCase): - pass From 8b95b61aa0b2e342443d120eb7322adcebd4164a Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 17 Jun 2019 14:17:21 +0530 Subject: [PATCH 42/50] fix: Remove unwanted code --- erpnext/erpnext_integrations/exotel_integration.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 10d533c8df..a42718602a 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -75,12 +75,6 @@ def make_a_call(from_number, to_number, caller_id): def get_exotel_settings(): return frappe.get_single('Exotel Settings') -@frappe.whitelist(allow_guest=True) -def get_phone_numbers(): - numbers = 'some number' - whitelist_numbers(numbers, 'for number') - return numbers - def whitelist_numbers(numbers, caller_id): endpoint = get_exotel_endpoint('CustomerWhitelist') response = requests.post(endpoint, data={ From 04d623ab66b939551c9f18087836f980dc064a2d Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 17 Jun 2019 15:20:28 +0530 Subject: [PATCH 43/50] fix: Get employee login ids directly from child table --- erpnext/crm/doctype/utils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index b424ac3878..f0f3e0844e 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -79,17 +79,20 @@ def add_call_summary(docname, summary): call_log.save(ignore_permissions=True) def get_employee_emails_for_popup(communication_medium): - employee_emails = [] now_time = frappe.utils.nowtime() weekday = frappe.utils.get_weekday() - available_employee_groups = frappe.db.sql("""SELECT `parent`, `employee_group` + available_employee_groups = frappe.db.sql_list("""SELECT `employee_group` FROM `tabCommunication Medium Timeslot` WHERE `day_of_week` = %s AND `parent` = %s AND %s BETWEEN `from_time` AND `to_time` - """, (weekday, communication_medium, now_time), as_dict=1) - for group in available_employee_groups: - employee_emails += [e.user_id for e in frappe.get_doc('Employee Group', group.employee_group).employee_list] + """, (weekday, communication_medium, now_time)) + + employees = frappe.get_all('Employee Group Table', filters={ + 'parent': ['in', available_employee_groups] + }, fields=['user_id']) + + employee_emails = set([employee.user_id for employee in employees]) return employee_emails From 9ba9a91631efdd5723b1d5039289410c15a65cfb Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 18 Jun 2019 10:56:47 +0530 Subject: [PATCH 44/50] fix: Strip leading zero from phone number --- erpnext/crm/doctype/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index f0f3e0844e..5f7a72e4aa 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -6,7 +6,7 @@ import json def get_document_with_phone_number(number): # finds contacts and leads if not number: return - number = number[-10:] + number = number.lstrip('0') number_filter = { 'phone': ['like', '%{}'.format(number)], 'mobile_no': ['like', '%{}'.format(number)] From a28b96493f83889e5dfa66b106ee597afedcfa53 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 18 Jun 2019 11:28:14 +0530 Subject: [PATCH 45/50] fix: Make field labels translatable --- erpnext/public/js/call_popup/call_popup.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 4117b6c00c..17bd74103e 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -16,15 +16,16 @@ class CallPopup { }, { 'fielname': 'last_interaction', 'fieldtype': 'Section Break', + 'label': __('Activity'), }, { 'fieldtype': 'Small Text', - 'label': "Last Communication", + 'label': __('Last Communication'), 'fieldname': 'last_communication', 'read_only': true, 'default': `${__('No communication found.')}` }, { 'fieldtype': 'Small Text', - 'label': "Last Issue", + 'label': __('Last Issue'), 'fieldname': 'last_issue', 'read_only': true, 'default': `${__('No issue raised by the customer.')}` @@ -32,11 +33,11 @@ class CallPopup { 'fieldtype': 'Column Break', }, { 'fieldtype': 'Small Text', - 'label': 'Call Summary', + 'label': __('Call Summary'), 'fieldname': 'call_summary', }, { 'fieldtype': 'Button', - 'label': 'Save', + 'label': __('Save'), 'click': () => { const call_summary = this.dialog.get_value('call_summary'); if (!call_summary) return; @@ -58,7 +59,7 @@ class CallPopup { this.dialog.get_close_btn().show(); this.dialog.$body.addClass('call-popup'); this.dialog.set_secondary_action(this.close_modal.bind(this)); - frappe.utils.play_sound("incoming-call"); + frappe.utils.play_sound('incoming-call'); this.dialog.show(); } @@ -157,7 +158,7 @@ class CallPopup { } call_disconnected(call_log) { - frappe.utils.play_sound("call-disconnect"); + frappe.utils.play_sound('call-disconnect'); this.update_call_log(call_log); setTimeout(() => { if (!this.dialog.get_value('call_summary')) { From d4420afb72dc21add02451f1f5c57a8a8d4caa7f Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 19 Jun 2019 18:33:56 +0530 Subject: [PATCH 46/50] chore: Remove unwanted files --- erpnext/.pylintrc | 1 + .../doctype/exotel_settings/exotel_settings.js | 8 -------- .../doctype/exotel_settings/test_exotel_settings.py | 10 ---------- 3 files changed, 1 insertion(+), 18 deletions(-) create mode 100644 erpnext/.pylintrc delete mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.js delete mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/test_exotel_settings.py diff --git a/erpnext/.pylintrc b/erpnext/.pylintrc new file mode 100644 index 0000000000..4b2ea0a564 --- /dev/null +++ b/erpnext/.pylintrc @@ -0,0 +1 @@ +disable=access-member-before-definition \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.js b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.js deleted file mode 100644 index bfed491d4b..0000000000 --- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Exotel Settings', { - // refresh: function(frm) { - - // } -}); diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/test_exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/test_exotel_settings.py deleted file mode 100644 index 5d85615c27..0000000000 --- a/erpnext/erpnext_integrations/doctype/exotel_settings/test_exotel_settings.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestExotelSettings(unittest.TestCase): - pass From 16baa3b13a09f90bb805872a480788e076f24973 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 19 Jun 2019 18:35:36 +0530 Subject: [PATCH 47/50] fix: Move back .pylintrc to root --- erpnext/.pylintrc => .pylintrc | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename erpnext/.pylintrc => .pylintrc (100%) diff --git a/erpnext/.pylintrc b/.pylintrc similarity index 100% rename from erpnext/.pylintrc rename to .pylintrc From 1431bf2a3cb0ce89284f5af24aa8cc058b60f7a4 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 1 Jul 2019 09:10:23 +0530 Subject: [PATCH 48/50] fix: User cannot create call log --- erpnext/communication/doctype/call_log/call_log.js | 8 ++++++++ erpnext/communication/doctype/call_log/call_log.json | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 erpnext/communication/doctype/call_log/call_log.js diff --git a/erpnext/communication/doctype/call_log/call_log.js b/erpnext/communication/doctype/call_log/call_log.js new file mode 100644 index 0000000000..0018516ec0 --- /dev/null +++ b/erpnext/communication/doctype/call_log/call_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Call Log', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json index c3d6d07fa5..110030d3de 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -79,7 +79,8 @@ "read_only": 1 } ], - "modified": "2019-06-17 09:02:48.150383", + "in_create": 1, + "modified": "2019-07-01 09:09:48.516722", "modified_by": "Administrator", "module": "Communication", "name": "Call Log", From 502565ff5641ceb68409abe4c662a2eb1804913c Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 1 Jul 2019 14:28:59 +0530 Subject: [PATCH 49/50] fix: Make required changes --- erpnext/crm/doctype/utils.py | 14 ++++--- .../exotel_integration.py | 38 ++++++++++--------- erpnext/public/js/call_popup/call_popup.js | 10 ++--- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 5f7a72e4aa..bd8b678d3b 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -82,12 +82,14 @@ def get_employee_emails_for_popup(communication_medium): now_time = frappe.utils.nowtime() weekday = frappe.utils.get_weekday() - available_employee_groups = frappe.db.sql_list("""SELECT `employee_group` - FROM `tabCommunication Medium Timeslot` - WHERE `day_of_week` = %s - AND `parent` = %s - AND %s BETWEEN `from_time` AND `to_time` - """, (weekday, communication_medium, now_time)) + available_employee_groups = frappe.get_all("Communication Medium Timeslot", filters={ + 'day_of_week': weekday, + 'parent': communication_medium, + 'from_time': ['<=', now_time], + 'to_time': ['>=', now_time], + }, fields=['employee_group'], debug=1) + + available_employee_groups = tuple([emp.employee_group for emp in available_employee_groups]) employees = frappe.get_all('Employee Group Table', filters={ 'parent': ['in', available_employee_groups] diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index a42718602a..c04cedce31 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -6,26 +6,29 @@ import requests # api/method/erpnext.erpnext_integrations.exotel_integration.handle_missed_call @frappe.whitelist(allow_guest=True) -def handle_incoming_call(*args, **kwargs): +def handle_incoming_call(**kwargs): exotel_settings = get_exotel_settings() if not exotel_settings.enabled: return - status = kwargs.get('Status') + call_payload = kwargs + status = call_payload.get('Status') if status == 'free': return - create_call_log(kwargs) + call_log = get_call_log(call_payload) + if not call_log: + create_call_log(call_payload) @frappe.whitelist(allow_guest=True) -def handle_end_call(*args, **kwargs): +def handle_end_call(**kwargs): update_call_log(kwargs, 'Completed') @frappe.whitelist(allow_guest=True) -def handle_missed_call(*args, **kwargs): +def handle_missed_call(**kwargs): update_call_log(kwargs, 'Missed') def update_call_log(call_payload, status): - call_log = get_call_log(call_payload, False) + call_log = get_call_log(call_payload) if call_log: call_log.status = status call_log.duration = call_payload.get('DialCallDuration') or 0 @@ -34,25 +37,24 @@ def update_call_log(call_payload, status): frappe.db.commit() return call_log -def get_call_log(call_payload, create_new_if_not_found=True): +def get_call_log(call_payload): call_log = frappe.get_all('Call Log', { 'id': call_payload.get('CallSid'), }, limit=1) if call_log: return frappe.get_doc('Call Log', call_log[0].name) - elif create_new_if_not_found: - call_log = frappe.new_doc('Call Log') - call_log.id = call_payload.get('CallSid') - call_log.to = call_payload.get('CallTo') - call_log.medium = call_payload.get('To') - call_log.status = 'Ringing' - setattr(call_log, 'from', call_payload.get('CallFrom')) - call_log.save(ignore_permissions=True) - frappe.db.commit() - return call_log -create_call_log = get_call_log +def create_call_log(call_payload): + call_log = frappe.new_doc('Call Log') + call_log.id = call_payload.get('CallSid') + call_log.to = call_payload.get('CallTo') + call_log.medium = call_payload.get('To') + call_log.status = 'Ringing' + setattr(call_log, 'from', call_payload.get('CallFrom')) + call_log.save(ignore_permissions=True) + frappe.db.commit() + return call_log @frappe.whitelist() def get_call_status(call_id): diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 17bd74103e..91dfe809a4 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -64,8 +64,8 @@ class CallPopup { } make_caller_info_section() { - const wrapper = this.dialog.fields_dict['caller_info'].$wrapper; - wrapper.append('
Loading...
'); + const wrapper = this.dialog.get_field('caller_info').$wrapper; + wrapper.append(`
${__("Loading...")}
`); frappe.xcall('erpnext.crm.doctype.utils.get_document_with_phone_number', { 'number': this.caller_number }).then(contact_doc => { @@ -88,7 +88,7 @@ class CallPopup {
@@ -172,7 +172,7 @@ class CallPopup { 'number': this.caller_number, 'reference_doc': this.contact }).then(data => { - const comm_field = this.dialog.fields_dict["last_communication"]; + const comm_field = this.dialog.get_field('last_communication'); if (data.last_communication) { const comm = data.last_communication; comm_field.set_value(comm.content); @@ -180,7 +180,7 @@ class CallPopup { if (data.last_issue) { const issue = data.last_issue; - const issue_field = this.dialog.fields_dict["last_issue"]; + const issue_field = this.dialog.get_field("last_issue"); issue_field.set_value(issue.subject); issue_field.$wrapper.append(` ${__('View all issues from {0}', [issue.customer])} From 8e65d5bd5b24557fd61e895950ef0bb32f5708ef Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 1 Jul 2019 14:32:29 +0530 Subject: [PATCH 50/50] chore: Delete unwanted files --- erpnext/communication/doctype/call_log/call_log.js | 8 -------- .../communication_medium/communication_medium.js | 8 -------- .../communication_medium/test_communication_medium.py | 10 ---------- 3 files changed, 26 deletions(-) delete mode 100644 erpnext/communication/doctype/call_log/call_log.js delete mode 100644 erpnext/communication/doctype/communication_medium/communication_medium.js delete mode 100644 erpnext/communication/doctype/communication_medium/test_communication_medium.py diff --git a/erpnext/communication/doctype/call_log/call_log.js b/erpnext/communication/doctype/call_log/call_log.js deleted file mode 100644 index 0018516ec0..0000000000 --- a/erpnext/communication/doctype/call_log/call_log.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Call Log', { - // refresh: function(frm) { - - // } -}); diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.js b/erpnext/communication/doctype/communication_medium/communication_medium.js deleted file mode 100644 index e37cd5b454..0000000000 --- a/erpnext/communication/doctype/communication_medium/communication_medium.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Communication Medium', { - // refresh: function(frm) { - - // } -}); diff --git a/erpnext/communication/doctype/communication_medium/test_communication_medium.py b/erpnext/communication/doctype/communication_medium/test_communication_medium.py deleted file mode 100644 index fc5754fe98..0000000000 --- a/erpnext/communication/doctype/communication_medium/test_communication_medium.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestCommunicationMedium(unittest.TestCase): - pass