mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 08:24:47 +00:00
Merge branch 'develop' into exotel-fixes
This commit is contained in:
@@ -11,8 +11,8 @@ from frappe.model.document import Document
|
||||
from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number
|
||||
from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup, strip_number
|
||||
|
||||
END_CALL_STATUSES = ['No Answer', 'Completed', 'Busy', 'Failed']
|
||||
ONGOING_CALL_STATUSES = ['Ringing', 'In Progress']
|
||||
END_CALL_STATUSES = ["No Answer", "Completed", "Busy", "Failed"]
|
||||
ONGOING_CALL_STATUSES = ["Ringing", "In Progress"]
|
||||
|
||||
|
||||
class CallLog(Document):
|
||||
@@ -20,23 +20,22 @@ class CallLog(Document):
|
||||
deduplicate_dynamic_links(self)
|
||||
|
||||
def before_insert(self):
|
||||
"""Add lead(third party person) links to the document.
|
||||
"""
|
||||
lead_number = self.get('from') if self.is_incoming_call() else self.get('to')
|
||||
"""Add lead(third party person) links to the document."""
|
||||
lead_number = self.get("from") if self.is_incoming_call() else self.get("to")
|
||||
lead_number = strip_number(lead_number)
|
||||
|
||||
contact = get_contact_with_phone_number(strip_number(lead_number))
|
||||
if contact:
|
||||
self.add_link(link_type='Contact', link_name=contact)
|
||||
self.add_link(link_type="Contact", link_name=contact)
|
||||
|
||||
lead = get_lead_with_phone_number(lead_number)
|
||||
if lead:
|
||||
self.add_link(link_type='Lead', link_name=lead)
|
||||
self.add_link(link_type="Lead", link_name=lead)
|
||||
|
||||
# Add Employee Name
|
||||
if self.is_incoming_call():
|
||||
# Taking the last 10 digits of the number
|
||||
employee_number = strip_number(self.get('to'))
|
||||
employee_number = strip_number(self.get("to"))
|
||||
employee = get_employees_with_number(self.get("to"))
|
||||
|
||||
self.call_received_by = employee[0].get("name")
|
||||
@@ -48,29 +47,29 @@ class CallLog(Document):
|
||||
def on_update(self):
|
||||
def _is_call_missed(doc_before_save, doc_after_save):
|
||||
# FIXME: This works for Exotel but not for all telepony providers
|
||||
return doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES
|
||||
return (
|
||||
doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES
|
||||
)
|
||||
|
||||
def _is_call_ended(doc_before_save, doc_after_save):
|
||||
return doc_before_save.status not in END_CALL_STATUSES and self.status in END_CALL_STATUSES
|
||||
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save: return
|
||||
if not doc_before_save:
|
||||
return
|
||||
|
||||
if _is_call_missed(doc_before_save, self):
|
||||
frappe.publish_realtime('call_{id}_missed'.format(id=self.id), self)
|
||||
frappe.publish_realtime("call_{id}_missed".format(id=self.id), self)
|
||||
self.trigger_call_popup()
|
||||
|
||||
if _is_call_ended(doc_before_save, self):
|
||||
frappe.publish_realtime('call_{id}_ended'.format(id=self.id), self)
|
||||
frappe.publish_realtime("call_{id}_ended".format(id=self.id), self)
|
||||
|
||||
def is_incoming_call(self):
|
||||
return self.type == 'Incoming'
|
||||
return self.type == "Incoming"
|
||||
|
||||
def add_link(self, link_type, link_name):
|
||||
self.append('links', {
|
||||
'link_doctype': link_type,
|
||||
'link_name': link_name
|
||||
})
|
||||
self.append("links", {"link_doctype": link_type, "link_name": link_name})
|
||||
|
||||
def trigger_call_popup(self):
|
||||
if self.is_incoming_call():
|
||||
@@ -82,61 +81,72 @@ class CallLog(Document):
|
||||
emails = set(scheduled_employees).intersection(employee_emails)
|
||||
|
||||
if frappe.conf.developer_mode:
|
||||
self.add_comment(text=f"""
|
||||
self.add_comment(
|
||||
text=f"""
|
||||
Scheduled Employees: {scheduled_employees}
|
||||
Matching Employee: {employee_emails}
|
||||
Show Popup To: {emails}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
if employee_emails and not emails:
|
||||
self.add_comment(text=_("No employee was scheduled for call popup"))
|
||||
|
||||
for email in emails:
|
||||
frappe.publish_realtime('show_call_popup', self, user=email)
|
||||
frappe.publish_realtime("show_call_popup", self, user=email)
|
||||
|
||||
|
||||
def get_employee_name(emp):
|
||||
employee_name = ''
|
||||
for name in ['first_name', 'middle_name', 'last_name']:
|
||||
employee_name = ""
|
||||
for name in ["first_name", "middle_name", "last_name"]:
|
||||
if emp.get(name):
|
||||
employee_name += (' ' if employee_name else '') + emp.get(name)
|
||||
employee_name += (" " if employee_name else "") + emp.get(name)
|
||||
return employee_name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_call_summary_and_call_type(call_log, summary, call_type):
|
||||
doc = frappe.get_doc('Call Log', call_log)
|
||||
doc = frappe.get_doc("Call Log", call_log)
|
||||
doc.type_of_call = call_type
|
||||
doc.save()
|
||||
doc.add_comment('Comment', frappe.bold(_('Call Summary')) + '<br><br>' + summary)
|
||||
doc.add_comment("Comment", frappe.bold(_("Call Summary")) + "<br><br>" + summary)
|
||||
|
||||
|
||||
def get_employees_with_number(number):
|
||||
number = strip_number(number)
|
||||
if not number: return []
|
||||
if not number:
|
||||
return []
|
||||
|
||||
employee_doc_name_and_emails = frappe.cache().hget('employees_with_number', number)
|
||||
if employee_doc_name_and_emails: return employee_doc_name_and_emails
|
||||
employee_doc_name_and_emails = frappe.cache().hget("employees_with_number", number)
|
||||
if employee_doc_name_and_emails:
|
||||
return employee_doc_name_and_emails
|
||||
|
||||
employee_doc_name_and_emails = frappe.get_all('Employee', filters={
|
||||
'cell_number': ['like', '%{}%'.format(number)],
|
||||
'user_id': ['!=', '']
|
||||
}, fields=['name', 'user_id'])
|
||||
employee_doc_name_and_emails = frappe.get_all(
|
||||
"Employee",
|
||||
filters={"cell_number": ["like", "%{}%".format(number)], "user_id": ["!=", ""]},
|
||||
fields=["name", "user_id"],
|
||||
)
|
||||
|
||||
frappe.cache().hset('employees_with_number', number, employee_doc_name_and_emails)
|
||||
frappe.cache().hset("employees_with_number", number, employee_doc_name_and_emails)
|
||||
|
||||
return employee_doc_name_and_emails
|
||||
|
||||
|
||||
def link_existing_conversations(doc, state):
|
||||
"""
|
||||
Called from hooks on creation of Contact or Lead to link all the existing conversations.
|
||||
"""
|
||||
if doc.doctype != 'Contact': return
|
||||
if doc.doctype != "Contact":
|
||||
return
|
||||
try:
|
||||
numbers = [d.phone for d in doc.phone_nos]
|
||||
|
||||
for number in numbers:
|
||||
number = strip_number(number)
|
||||
if not number: continue
|
||||
logs = frappe.db.sql_list("""
|
||||
if not number:
|
||||
continue
|
||||
logs = frappe.db.sql_list(
|
||||
"""
|
||||
SELECT cl.name FROM `tabCall Log` cl
|
||||
LEFT JOIN `tabDynamic Link` dl
|
||||
ON cl.name = dl.parent
|
||||
@@ -149,44 +159,42 @@ def link_existing_conversations(doc, state):
|
||||
ELSE 0
|
||||
END
|
||||
)=0
|
||||
""", dict(
|
||||
phone_number='%{}'.format(number),
|
||||
docname=doc.name,
|
||||
doctype = doc.doctype
|
||||
)
|
||||
""",
|
||||
dict(phone_number="%{}".format(number), docname=doc.name, doctype=doc.doctype),
|
||||
)
|
||||
|
||||
for log in logs:
|
||||
call_log = frappe.get_doc('Call Log', log)
|
||||
call_log = frappe.get_doc("Call Log", log)
|
||||
call_log.add_link(link_type=doc.doctype, link_name=doc.name)
|
||||
call_log.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
except Exception:
|
||||
frappe.log_error(title=_('Error during caller information update'))
|
||||
frappe.log_error(title=_("Error during caller information update"))
|
||||
|
||||
|
||||
def get_linked_call_logs(doctype, docname):
|
||||
# content will be shown in timeline
|
||||
logs = frappe.get_all('Dynamic Link', fields=['parent'], filters={
|
||||
'parenttype': 'Call Log',
|
||||
'link_doctype': doctype,
|
||||
'link_name': docname
|
||||
})
|
||||
logs = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
fields=["parent"],
|
||||
filters={"parenttype": "Call Log", "link_doctype": doctype, "link_name": docname},
|
||||
)
|
||||
|
||||
logs = set([log.parent for log in logs])
|
||||
|
||||
logs = frappe.get_all('Call Log', fields=['*'], filters={
|
||||
'name': ['in', logs]
|
||||
})
|
||||
logs = frappe.get_all("Call Log", fields=["*"], filters={"name": ["in", logs]})
|
||||
|
||||
timeline_contents = []
|
||||
for log in logs:
|
||||
log.show_call_button = 0
|
||||
timeline_contents.append({
|
||||
'icon': 'call',
|
||||
'is_card': True,
|
||||
'creation': log.creation,
|
||||
'template': 'call_link',
|
||||
'template_data': log
|
||||
})
|
||||
timeline_contents.append(
|
||||
{
|
||||
"icon": "call",
|
||||
"is_card": True,
|
||||
"creation": log.creation,
|
||||
"template": "call_link",
|
||||
"template_data": log,
|
||||
}
|
||||
)
|
||||
|
||||
return timeline_contents
|
||||
|
||||
@@ -20,35 +20,38 @@ class IncomingCallSettings(Document):
|
||||
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.
|
||||
"""
|
||||
"""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)
|
||||
_("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))
|
||||
frappe.throw("<br/>".join(errors))
|
||||
|
||||
def validate_call_schedule_overlaps(self, schedule: list):
|
||||
"""Check if any time slots are overlapped in a day schedule.
|
||||
"""
|
||||
"""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]
|
||||
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
|
||||
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))
|
||||
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:
|
||||
@@ -58,7 +61,6 @@ class IncomingCallSettings(Document):
|
||||
|
||||
@staticmethod
|
||||
def time_to_seconds(time: str) -> int:
|
||||
"""Convert time string of format HH:MM:SS into seconds
|
||||
"""
|
||||
"""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)
|
||||
|
||||
Reference in New Issue
Block a user