feat: Introducing telephony module (#24032)
This commit is contained in:
parent
ad57eef40c
commit
a3845a95ed
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@ -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
|
@ -271,11 +271,11 @@ doc_events = {
|
||||
},
|
||||
"Contact": {
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
|
||||
|
@ -25,4 +25,5 @@ Hub Node
|
||||
Quality Management
|
||||
Communication
|
||||
Loan Management
|
||||
Payroll
|
||||
Payroll
|
||||
Telephony
|
@ -49,7 +49,8 @@
|
||||
"public/js/education/assessment_result_tool.html",
|
||||
"public/js/hub/hub_factory.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": [
|
||||
"stock/dashboard/item_dashboard.html",
|
||||
|
@ -74,7 +74,7 @@ class CallPopup {
|
||||
'click': () => {
|
||||
const call_summary = this.dialog.get_value('call_summary');
|
||||
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,
|
||||
'summary': call_summary,
|
||||
}).then(() => {
|
||||
|
23
erpnext/public/js/telephony.js
Normal file
23
erpnext/public/js/telephony.js
Normal file
@ -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/telephony/doctype/__init__.py
Normal file
0
erpnext/telephony/doctype/__init__.py
Normal file
0
erpnext/telephony/doctype/call_log/__init__.py
Normal file
0
erpnext/telephony/doctype/call_log/__init__.py
Normal file
8
erpnext/telephony/doctype/call_log/call_log.js
Normal file
8
erpnext/telephony/doctype/call_log/call_log.js
Normal file
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
@ -137,12 +137,11 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-25 17:08:34.085731",
|
||||
"modified": "2020-11-25 14:32:44.407815",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Communication",
|
||||
"module": "Telephony",
|
||||
"name": "Call Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
10
erpnext/telephony/doctype/call_log/test_call_log.py
Normal file
10
erpnext/telephony/doctype/call_log/test_call_log.py
Normal file
@ -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,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
|
||||
}
|
@ -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,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);
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
@ -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…
x
Reference in New Issue
Block a user