Browse Source

feat: Introducing telephony module (#24032)

develop
Leela vadlamudi 4 years ago
committed by GitHub
parent
commit
a3845a95ed
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      .editorconfig
  2. 4
      erpnext/hooks.py
  3. 1
      erpnext/modules.txt
  4. 3
      erpnext/public/build.json
  5. 2
      erpnext/public/js/call_popup/call_popup.js
  6. 23
      erpnext/public/js/telephony.js
  7. 0
      erpnext/telephony/__init__.py
  8. 0
      erpnext/telephony/doctype/__init__.py
  9. 0
      erpnext/telephony/doctype/call_log/__init__.py
  10. 8
      erpnext/telephony/doctype/call_log/call_log.js
  11. 5
      erpnext/telephony/doctype/call_log/call_log.json
  12. 0
      erpnext/telephony/doctype/call_log/call_log.py
  13. 10
      erpnext/telephony/doctype/call_log/test_call_log.py
  14. 0
      erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py
  15. 60
      erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json
  16. 10
      erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py
  17. 0
      erpnext/telephony/doctype/incoming_call_settings/__init__.py
  18. 102
      erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js
  19. 82
      erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json
  20. 63
      erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py
  21. 10
      erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py

14
.editorconfig

@ -0,0 +1,14 @@
# Root editor config file
root = true
# Common settings
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
# python, js indentation settings
[{*.py,*.js}]
indent_style = tab
indent_size = 4

4
erpnext/hooks.py

@ -271,11 +271,11 @@ doc_events = {
}, },
"Contact": { "Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue", "on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information", "after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information",
"validate": "erpnext.crm.utils.update_lead_phone_numbers" "validate": "erpnext.crm.utils.update_lead_phone_numbers"
}, },
"Lead": { "Lead": {
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information" "after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information"
}, },
"Email Unsubscribe": { "Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"

1
erpnext/modules.txt

@ -26,3 +26,4 @@ Quality Management
Communication Communication
Loan Management Loan Management
Payroll Payroll
Telephony

3
erpnext/public/build.json

@ -49,7 +49,8 @@
"public/js/education/assessment_result_tool.html", "public/js/education/assessment_result_tool.html",
"public/js/hub/hub_factory.js", "public/js/hub/hub_factory.js",
"public/js/call_popup/call_popup.js", "public/js/call_popup/call_popup.js",
"public/js/utils/dimension_tree_filter.js" "public/js/utils/dimension_tree_filter.js",
"public/js/telephony.js"
], ],
"js/item-dashboard.min.js": [ "js/item-dashboard.min.js": [
"stock/dashboard/item_dashboard.html", "stock/dashboard/item_dashboard.html",

2
erpnext/public/js/call_popup/call_popup.js

@ -74,7 +74,7 @@ class CallPopup {
'click': () => { 'click': () => {
const call_summary = this.dialog.get_value('call_summary'); const call_summary = this.dialog.get_value('call_summary');
if (!call_summary) return; if (!call_summary) return;
frappe.xcall('erpnext.communication.doctype.call_log.call_log.add_call_summary', { frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', {
'call_log': this.call_log.name, 'call_log': this.call_log.name,
'summary': call_summary, 'summary': call_summary,
}).then(() => { }).then(() => {

23
erpnext/public/js/telephony.js

@ -0,0 +1,23 @@
frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( {
make_input() {
this._super();
if (this.df.options == 'Phone') {
this.setup_phone();
}
},
setup_phone() {
if (frappe.phone_call.handler) {
this.$wrapper.find('.control-input')
.append(`
<span class="phone-btn">
<a class="btn-open no-decoration" title="${__('Make a call')}">
<i class="fa fa-phone"></i></a>
</span>
`)
.find('.phone-btn')
.click(() => {
frappe.phone_call.handler(this.get_value(), this.frm);
});
}
}
});

0
erpnext/communication/doctype/call_log/__init__.py → erpnext/telephony/__init__.py

0
erpnext/telephony/doctype/__init__.py

0
erpnext/telephony/doctype/call_log/__init__.py

8
erpnext/telephony/doctype/call_log/call_log.js

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Call Log', {
// refresh: function(frm) {
// }
});

5
erpnext/communication/doctype/call_log/call_log.json → erpnext/telephony/doctype/call_log/call_log.json

@ -137,12 +137,11 @@
"read_only": 1 "read_only": 1
} }
], ],
"in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-08-25 17:08:34.085731", "modified": "2020-11-25 14:32:44.407815",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Communication", "module": "Telephony",
"name": "Call Log", "name": "Call Log",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

0
erpnext/communication/doctype/call_log/call_log.py → erpnext/telephony/doctype/call_log/call_log.py

10
erpnext/telephony/doctype/call_log/test_call_log.py

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestCallLog(unittest.TestCase):
pass

0
erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py

60
erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json

@ -0,0 +1,60 @@
{
"actions": [],
"creation": "2020-11-19 11:15:54.967710",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"day_of_week",
"from_time",
"to_time",
"agent_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
},
{
"default": "9:00:00",
"fieldname": "from_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "From Time",
"reqd": 1
},
{
"default": "17:00:00",
"fieldname": "to_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "To Time",
"reqd": 1
},
{
"fieldname": "agent_group",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Agent Group",
"options": "Employee Group",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-19 11:15:54.967710",
"modified_by": "Administrator",
"module": "Telephony",
"name": "Incoming Call Handling Schedule",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

10
erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, 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 IncomingCallHandlingSchedule(Document):
pass

0
erpnext/telephony/doctype/incoming_call_settings/__init__.py

102
erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js

@ -0,0 +1,102 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
function time_to_seconds(time_str) {
// Convert time string of format HH:MM:SS into seconds.
let seq = time_str.split(':');
seq = seq.map((n) => parseInt(n));
return (seq[0]*60*60) + (seq[1]*60) + seq[2];
}
function number_sort(array, ascending=true) {
let array_copy = [...array];
if (ascending) {
array_copy.sort((a, b) => a-b); // ascending order
} else {
array_copy.sort((a, b) => b-a); // descending order
}
return array_copy;
}
function groupby(items, key) {
// Group the list of items using the given key.
const obj = {};
items.forEach((item) => {
if (item[key] in obj) {
obj[item[key]].push(item);
} else {
obj[item[key]] = [item];
}
});
return obj;
}
function check_timeslot_overlap(ts1, ts2) {
/// Timeslot is a an array of length 2 ex: [from_time, to_time]
/// time in timeslot is an integer represents number of seconds.
if ((ts1[0] < ts2[0] && ts1[1] <= ts2[0]) || (ts1[0] >= ts2[1] && ts1[1] > ts2[1])) {
return false;
}
return true;
}
function validate_call_schedule(schedule) {
validate_call_schedule_timeslot(schedule);
validate_call_schedule_overlaps(schedule);
}
function validate_call_schedule_timeslot(schedule) {
// Make sure that to time slot is ahead of from time slot.
let errors = [];
for (let row in schedule) {
let record = schedule[row];
let from_time_in_secs = time_to_seconds(record.from_time);
let to_time_in_secs = time_to_seconds(record.to_time);
if (from_time_in_secs >= to_time_in_secs) {
errors.push(__('Call Schedule Row {0}: To time slot should always be ahead of From time slot.', [row]));
}
}
if (errors.length > 0) {
frappe.throw(errors.join("<br/>"));
}
}
function is_call_schedule_overlapped(day_schedule) {
// Check if any time slots are overlapped in a day schedule.
let timeslots = [];
day_schedule.forEach((record)=> {
timeslots.push([time_to_seconds(record.from_time), time_to_seconds(record.to_time)]);
});
if (timeslots.length < 2) {
return false;
}
timeslots = number_sort(timeslots);
// Sorted timeslots will be in ascending order if not overlapped.
for (let i=1; i < timeslots.length; i++) {
if (check_timeslot_overlap(timeslots[i-1], timeslots[i])) {
return true;
}
}
return false;
}
function validate_call_schedule_overlaps(schedule) {
let group_by_day = groupby(schedule, 'day_of_week');
for (const [day, day_schedule] of Object.entries(group_by_day)) {
if (is_call_schedule_overlapped(day_schedule)) {
frappe.throw(__('Please fix overlapping time slots for {0}', [day]));
}
}
}
frappe.ui.form.on('Incoming Call Settings', {
validate(frm) {
validate_call_schedule(frm.doc.call_handling_schedule);
}
});

82
erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json

@ -0,0 +1,82 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2020-11-19 10:37:20.734245",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"call_routing",
"column_break_2",
"greeting_message",
"agent_busy_message",
"agent_unavailable_message",
"section_break_6",
"call_handling_schedule"
],
"fields": [
{
"default": "Sequential",
"fieldname": "call_routing",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Call Routing",
"options": "Sequential\nSimultaneous"
},
{
"fieldname": "greeting_message",
"fieldtype": "Data",
"label": "Greeting Message"
},
{
"fieldname": "agent_busy_message",
"fieldtype": "Data",
"label": "Agent Busy Message"
},
{
"fieldname": "agent_unavailable_message",
"fieldtype": "Data",
"label": "Agent Unavailable Message"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "call_handling_schedule",
"fieldtype": "Table",
"label": "Call Handling Schedule",
"options": "Incoming Call Handling Schedule",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-11-19 11:17:14.527862",
"modified_by": "Administrator",
"module": "Telephony",
"name": "Incoming Call Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

63
erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, 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
from datetime import datetime
from typing import Tuple
from frappe import _
class IncomingCallSettings(Document):
def validate(self):
"""List of validations
* Make sure that to time slot is ahead of from time slot in call schedule
* Make sure that no overlapping timeslots for a given day
"""
self.validate_call_schedule_timeslot(self.call_handling_schedule)
self.validate_call_schedule_overlaps(self.call_handling_schedule)
def validate_call_schedule_timeslot(self, schedule: list):
""" Make sure that to time slot is ahead of from time slot.
"""
errors = []
for record in schedule:
from_time = self.time_to_seconds(record.from_time)
to_time = self.time_to_seconds(record.to_time)
if from_time >= to_time:
errors.append(
_('Call Schedule Row {0}: To time slot should always be ahead of From time slot.').format(record.idx)
)
if errors:
frappe.throw('<br/>'.join(errors))
def validate_call_schedule_overlaps(self, schedule: list):
"""Check if any time slots are overlapped in a day schedule.
"""
week_days = set([each.day_of_week for each in schedule])
for day in week_days:
timeslots = [(record.from_time, record.to_time) for record in schedule if record.day_of_week==day]
# convert time in timeslot into an integer represents number of seconds
timeslots = sorted(map(lambda seq: tuple(map(self.time_to_seconds, seq)), timeslots))
if len(timeslots) < 2: continue
for i in range(1, len(timeslots)):
if self.check_timeslots_overlap(timeslots[i-1], timeslots[i]):
frappe.throw(_('Please fix overlapping time slots for {0}.').format(day))
@staticmethod
def check_timeslots_overlap(ts1: Tuple[int, int], ts2: Tuple[int, int]) -> bool:
if (ts1[0] < ts2[0] and ts1[1] <= ts2[0]) or (ts1[0] >= ts2[1] and ts1[1] > ts2[1]):
return False
return True
@staticmethod
def time_to_seconds(time: str) -> int:
"""Convert time string of format HH:MM:SS into seconds
"""
date_time = datetime.strptime(time, "%H:%M:%S")
return date_time - datetime(1900, 1, 1)

10
erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestIncomingCallSettings(unittest.TestCase):
pass
Loading…
Cancel
Save