From 9751f1060e3ca686fdc8535543dffc44a37f76f8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 9 Aug 2022 19:35:43 +0530 Subject: [PATCH 01/29] fix: limit pos recent order page result (cherry picked from commit bb40e38451b37c6d855115411cb428e3a2e39c55) --- erpnext/selling/page/point_of_sale/point_of_sale.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index bf629824ad9..8bce1f60725 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -272,16 +272,20 @@ def get_past_order_list(search_term, status, limit=20): "POS Invoice", filters={"customer": ["like", "%{}%".format(search_term)], "status": status}, fields=fields, + page_length=limit, ) invoices_by_name = frappe.db.get_all( "POS Invoice", filters={"name": ["like", "%{}%".format(search_term)], "status": status}, fields=fields, + page_length=limit, ) invoice_list = invoices_by_customer + invoices_by_name elif status: - invoice_list = frappe.db.get_all("POS Invoice", filters={"status": status}, fields=fields) + invoice_list = frappe.db.get_all( + "POS Invoice", filters={"status": status}, fields=fields, page_length=limit + ) return invoice_list From acbed434c28ec0373c3ff4cd5489c7ea75a5581a Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Mon, 8 Aug 2022 16:29:13 +0530 Subject: [PATCH 02/29] fix: process loan interest accrual (cherry picked from commit 9ef8d5c5c314ec03f46e982ce8aa36487dbb7559) --- .../doctype/loan_interest_accrual/loan_interest_accrual.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index ef53303f5a1..1ed80087201 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -135,7 +135,11 @@ def calculate_accrual_amount_for_demand_loans( def make_accrual_interest_entry_for_demand_loans( posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular" ): - query_filters = {"status": ("in", ["Disbursed", "Partially Disbursed"]), "docstatus": 1} + query_filters = { + "status": ("in", ["Disbursed", "Partially Disbursed"]), + "docstatus": 1, + "is_term_loan": 0, + } if loan_type: query_filters.update({"loan_type": loan_type}) From 8dea238d124299d1ca21cdc7afb277cdf4138a97 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Mon, 8 Aug 2022 17:35:31 +0530 Subject: [PATCH 03/29] fix: term loan interest calculation (cherry picked from commit 534d7ce64b47c6c9272f7bedf79f4cdce825e0f2) --- .../doctype/loan_interest_accrual/loan_interest_accrual.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 1ed80087201..c347704b5b0 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -236,6 +236,7 @@ def get_term_loans(date, term_loan=None, loan_type=None): AND l.is_term_loan =1 AND rs.payment_date <= %s AND rs.is_accrued=0 {0} + AND rs.interest_amount > 0 AND l.status = 'Disbursed' ORDER BY rs.payment_date""".format( condition From ba5ee1ca96cebbe06b12a4bcd6f4a6a3941944ed Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 16 Aug 2022 16:39:28 +0530 Subject: [PATCH 04/29] fix: incorrect tax amt due to different exchange rate in PR and PI (cherry picked from commit 5fd0770372c07dbec89669e1dcdb4d98f1fbca90) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f6635516a10..96ee05d2304 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1689,4 +1689,6 @@ def make_purchase_receipt(source_name, target_doc=None): target_doc, ) + doc.set_onload("ignore_price_list", True) + return doc From 36130c62926814bd74412ff4c10e9c670157f82c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 18 Aug 2022 11:42:29 +0530 Subject: [PATCH 05/29] fix(minor): save the employee doc on promotion/transfer update --- erpnext/hr/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index b80226b224c..d256f34732e 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -224,7 +224,7 @@ def delete_employee_work_history(details, employee, date): filters["from_date"] = date if filters: frappe.db.delete("Employee Internal Work History", filters) - employee.reload() + employee.save() @frappe.whitelist() From a4828407d09cd41088120671e000afa3c6c484a4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 18 Aug 2022 12:02:32 +0530 Subject: [PATCH 06/29] fix: interview rescheduling not working --- erpnext/hr/doctype/interview/interview.py | 11 ++++++++--- erpnext/hr/doctype/interview/test_interview.py | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/interview/interview.py b/erpnext/hr/doctype/interview/interview.py index 8f0ff021335..a1d5c0249ba 100644 --- a/erpnext/hr/doctype/interview/interview.py +++ b/erpnext/hr/doctype/interview/interview.py @@ -94,8 +94,8 @@ class Interview(Document): @frappe.whitelist() def reschedule_interview(self, scheduled_on, from_time, to_time): original_date = self.scheduled_on - from_time = self.from_time - to_time = self.to_time + original_from_time = self.from_time + original_to_time = self.to_time self.db_set({"scheduled_on": scheduled_on, "from_time": from_time, "to_time": to_time}) self.notify_update() @@ -107,7 +107,12 @@ class Interview(Document): recipients=recipients, subject=_("Interview: {0} Rescheduled").format(self.name), message=_("Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}").format( - original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time + original_date, + original_from_time, + original_to_time, + self.scheduled_on, + self.from_time, + self.to_time, ), reference_doctype=self.doctype, reference_name=self.name, diff --git a/erpnext/hr/doctype/interview/test_interview.py b/erpnext/hr/doctype/interview/test_interview.py index 840a5ad919d..1062003dd8c 100644 --- a/erpnext/hr/doctype/interview/test_interview.py +++ b/erpnext/hr/doctype/interview/test_interview.py @@ -8,7 +8,7 @@ import unittest import frappe from frappe import _ from frappe.core.doctype.user_permission.test_user_permission import create_user -from frappe.utils import add_days, getdate, nowtime +from frappe.utils import add_days, get_time, getdate, nowtime from erpnext.hr.doctype.designation.test_designation import create_designation from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError @@ -26,18 +26,23 @@ class TestInterview(unittest.TestCase): def test_notification_on_rescheduling(self): job_applicant = create_job_applicant() interview = create_interview_and_dependencies( - job_applicant.name, scheduled_on=add_days(getdate(), -4) + job_applicant.name, + scheduled_on=add_days(getdate(), -4), + from_time="10:00:00", + to_time="11:00:00", ) previous_scheduled_date = interview.scheduled_on frappe.db.sql("DELETE FROM `tabEmail Queue`") interview.reschedule_interview( - add_days(getdate(previous_scheduled_date), 2), from_time=nowtime(), to_time=nowtime() + add_days(getdate(previous_scheduled_date), 2), from_time="11:00:00", to_time="12:00:00" ) interview.reload() self.assertEqual(interview.scheduled_on, add_days(getdate(previous_scheduled_date), 2)) + self.assertEqual(get_time(interview.from_time), get_time("11:00:00")) + self.assertEqual(get_time(interview.to_time), get_time("12:00:00")) notification = frappe.get_all( "Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")} From 6405a9378d2375d1db3a9defb8337207e35a1135 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 16 Aug 2022 09:27:42 +0530 Subject: [PATCH 07/29] fix: incorrect buying amount in Gross Profit rpt (cherry picked from commit 967dd398e7ff7207aaa4c7f5808bcac6084d44cb) --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2bc5208993d..7f6e2b99c89 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -561,7 +561,7 @@ class GrossProfitGenerator(object): previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0 if previous_stock_value: - return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) + return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) else: From a4521437825a960e14556fa3963bd1bd1a55a2dc Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Fri, 19 Aug 2022 12:30:47 +0530 Subject: [PATCH 08/29] fix: TDS calculation for advance payment "against_voucher": ["is", "not set"] was used in query due to which if TDS was added on "advance" payment vouchers and then reconciled against purchase invoice. it will not find those vouchers and consider this as first-time threshold due to which it will calculate Tax for all transactions. --- .../doctype/tax_withholding_category/tax_withholding_category.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 63698439be1..3db21dc5a41 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -309,7 +309,6 @@ def get_advance_vouchers( "is_cancelled": 0, "party_type": party_type, "party": ["in", parties], - "against_voucher": ["is", "not set"], } if company: From ffb1196516559557062422e9eeabc9c9aabd5f6f Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 23 Feb 2022 12:59:38 +0530 Subject: [PATCH 09/29] fix: call status fix --- .../erpnext_integrations/exotel_integration.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 1f5df67e197..d37400cb352 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -37,11 +37,24 @@ def handle_end_call(**kwargs): @frappe.whitelist(allow_guest=True) def handle_missed_call(**kwargs): - update_call_log(kwargs, "Missed") + status = "" + CallType = kwargs.get("CallType") + DialCallStatus = kwargs.get("DialCallStatus") + + if CallType == "incomplete" and DialCallStatus == "no-answer": + status = 'No Answer' + elif CallType == "client-hangup" and DialCallStatus == "canceled": + status = 'Canceled' + + update_call_log(kwargs, status) def update_call_log(call_payload, status="Ringing", call_log=None): call_log = call_log or get_call_log(call_payload) + + # for a new sid, call_log and get_call_log will be empty so create a new log + if not call_log: + call_log = create_call_log(call_payload) if call_log: call_log.status = status call_log.to = call_payload.get("DialWhomNumber") From 476ded2972b3a2cd3b8e379229a42cd791c45199 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 23 Feb 2022 18:45:50 +0530 Subject: [PATCH 10/29] fix: added employee name to call log --- erpnext/telephony/doctype/call_log/call_log.json | 14 ++++++++++++-- erpnext/telephony/doctype/call_log/call_log.py | 9 +++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index 1d6c39edf6e..a3dbb02213f 100644 --- a/erpnext/telephony/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "field:id", - "creation": "2019-06-05 12:07:02.634534", + "creation": "2022-02-21 11:54:58.414784", "doctype": "DocType", "engine": "InnoDB", "field_order": [ @@ -9,6 +9,7 @@ "id", "from", "to", + "employee_call_directed_to", "medium", "start_time", "end_time", @@ -134,15 +135,23 @@ "fieldname": "call_details_section", "fieldtype": "Section Break", "label": "Call Details" + }, + { + "depends_on": "to", + "fieldname": "employee_call_directed_to", + "fieldtype": "Data", + "label": "Employee Call Directed To", + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-08 14:23:28.744844", + "modified": "2022-02-23 18:45:06.932571", "modified_by": "Administrator", "module": "Telephony", "name": "Call Log", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -164,6 +173,7 @@ ], "sort_field": "creation", "sort_order": "DESC", + "states": [], "title_field": "from", "track_changes": 1, "track_views": 1 diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 1c88883abce..9154f3d2bcf 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -32,6 +32,15 @@ class CallLog(Document): if 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 + emp_number_reversed = (self.get("to"))[-1:-11:-1] + emp_number = emp_number_reversed[-1::-1] + + emp_name = frappe.get_all("Employee", filters={"cell_number":["like","%"+emp_number+"%"]}, fields=["first_name", "middle_name", "last_name"]) + self.employee_call_directed_to = (emp_name[0].get("first_name") or '') + ' ' + (emp_name[0].get("middle_name") or '') + ' ' + (emp_name[0].get("last_name") or '') + def after_insert(self): self.trigger_call_popup() From 66578a58cf78647c8eb8b1be21289211f7645737 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Fri, 25 Feb 2022 12:01:53 +0530 Subject: [PATCH 11/29] fix: added field to show called group, user_id --- erpnext/telephony/doctype/call_log/call_log.json | 9 ++++++++- erpnext/telephony/doctype/call_log/call_log.py | 12 ++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index a3dbb02213f..c0f022b0209 100644 --- a/erpnext/telephony/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -10,6 +10,7 @@ "from", "to", "employee_call_directed_to", + "employee_user_id", "medium", "start_time", "end_time", @@ -142,12 +143,18 @@ "fieldtype": "Data", "label": "Employee Call Directed To", "read_only": 1 + }, + { + "fieldname": "employee_user_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Employee User Id" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-02-23 18:45:06.932571", + "modified": "2022-02-23 19:47:04.310577", "modified_by": "Administrator", "module": "Telephony", "name": "Call Log", diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 9154f3d2bcf..9116c8b5114 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -38,8 +38,9 @@ class CallLog(Document): emp_number_reversed = (self.get("to"))[-1:-11:-1] emp_number = emp_number_reversed[-1::-1] - emp_name = frappe.get_all("Employee", filters={"cell_number":["like","%"+emp_number+"%"]}, fields=["first_name", "middle_name", "last_name"]) - self.employee_call_directed_to = (emp_name[0].get("first_name") or '') + ' ' + (emp_name[0].get("middle_name") or '') + ' ' + (emp_name[0].get("last_name") or '') + employee = frappe.get_all("Employee", filters={"cell_number":["like","%"+emp_number+"%"]}, fields=["first_name", "middle_name", "last_name", "user_id"]) + self.employee_call_directed_to = get_employee_name(employee[0]) + self.employee_user_id = employee[0].get("user_id") or '' def after_insert(self): self.trigger_call_popup() @@ -94,6 +95,13 @@ class CallLog(Document): for email in emails: 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']: + if emp.get(name): + employee_name += (' ' if employee_name else '') + emp.get(name) + return employee_name + @frappe.whitelist() def add_call_summary(call_log, summary): From b65bce8f986ae6050e14f40c449817b79e997850 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Fri, 25 Feb 2022 13:38:57 +0530 Subject: [PATCH 12/29] fix: added type of call select field, additional status for agent rejecting call --- erpnext/erpnext_integrations/exotel_integration.py | 2 ++ erpnext/public/js/call_popup/call_popup.js | 13 ++++++++++++- erpnext/telephony/doctype/call_log/call_log.json | 11 +++++++++-- erpnext/telephony/doctype/call_log/call_log.py | 9 +++++---- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index d37400cb352..3ddea15ed1d 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -45,6 +45,8 @@ def handle_missed_call(**kwargs): status = 'No Answer' elif CallType == "client-hangup" and DialCallStatus == "canceled": status = 'Canceled' + elif CallType == "incomplete" and DialCallStatus == "failed": + status = 'Failed' update_call_log(kwargs, status) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index c954f12ac63..2addeae0e5e 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -141,6 +141,15 @@ class CallPopup { 'fieldtype': 'Section Break', 'hide_border': 1, }, { + 'fieldname': 'type_of_call', + 'label': 'Type Of Call', + 'fieldtype': 'Select', + 'options': '\nFrappe Cloud Queries/Plan\nEnterprise Plans\nPartnership\nSupport\nBilling/Renewal\nOpen source / Junk', + 'default': 'Frappe Cloud Queries/Plan', + },{ + 'fieldtype': 'Section Break', + 'hide_border': 1, + },{ 'fieldtype': 'Small Text', 'label': __('Call Summary'), 'fieldname': 'call_summary', @@ -149,10 +158,12 @@ class CallPopup { 'label': __('Save'), 'click': () => { const call_summary = this.call_details.get_value('call_summary'); + const call_type = this.call_details.get_value('type_of_call'); if (!call_summary) return; - frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', { + frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary_and_call_type', { 'call_log': this.call_log.name, 'summary': call_summary, + 'call_type': call_type, }).then(() => { this.close_modal(); frappe.show_alert({ diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index c0f022b0209..615e069e72f 100644 --- a/erpnext/telephony/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -22,6 +22,7 @@ "recording_url", "recording_html", "section_break_11", + "type_of_call", "summary", "section_break_19", "links" @@ -105,7 +106,8 @@ }, { "fieldname": "summary", - "fieldtype": "Small Text" + "fieldtype": "Small Text", + "label": "Summary" }, { "fieldname": "section_break_11", @@ -149,12 +151,17 @@ "fieldtype": "Data", "hidden": 1, "label": "Employee User Id" + }, + { + "fieldname": "type_of_call", + "fieldtype": "Data", + "label": "Type Of Call" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-02-23 19:47:04.310577", + "modified": "2022-02-25 13:37:48.156501", "modified_by": "Administrator", "module": "Telephony", "name": "Call Log", diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 9116c8b5114..768bf5bde22 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -104,10 +104,11 @@ def get_employee_name(emp): @frappe.whitelist() -def add_call_summary(call_log, summary): - doc = frappe.get_doc("Call Log", call_log) - doc.add_comment("Comment", frappe.bold(_("Call Summary")) + "

" + summary) - +def add_call_summary_and_call_type(call_log, summary, call_type): + doc = frappe.get_doc('Call Log', call_log) + doc.type_of_call = call_type + doc.save() + doc.add_comment('Comment', frappe.bold(_('Call Summary')) + '

' + summary) def get_employees_with_number(number): number = strip_number(number) From 90ed14a055c0e625c3c2a74506d206cec3d7daa5 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Fri, 25 Feb 2022 13:45:26 +0530 Subject: [PATCH 13/29] fix: sider fixes --- erpnext/public/js/call_popup/call_popup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 2addeae0e5e..b7d3485b657 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -146,10 +146,10 @@ class CallPopup { 'fieldtype': 'Select', 'options': '\nFrappe Cloud Queries/Plan\nEnterprise Plans\nPartnership\nSupport\nBilling/Renewal\nOpen source / Junk', 'default': 'Frappe Cloud Queries/Plan', - },{ + }, { 'fieldtype': 'Section Break', 'hide_border': 1, - },{ + }, { 'fieldtype': 'Small Text', 'label': __('Call Summary'), 'fieldname': 'call_summary', From b94154f00aef71e10640531e1dbdef2c4a3c6e48 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Fri, 25 Feb 2022 16:52:25 +0530 Subject: [PATCH 14/29] fix: call type doctype, fixes --- .../exotel_integration.py | 10 ++-- erpnext/public/js/call_popup/call_popup.js | 11 ++-- .../telephony/doctype/call_log/call_log.json | 18 +++--- .../telephony/doctype/call_log/call_log.py | 6 +- .../doctype/telephony_call_type/__init__.py | 0 .../telephony_call_type.js | 8 +++ .../telephony_call_type.json | 58 +++++++++++++++++++ .../telephony_call_type.py | 8 +++ .../test_telephony_call_type.py | 8 +++ 9 files changed, 105 insertions(+), 22 deletions(-) create mode 100644 erpnext/telephony/doctype/telephony_call_type/__init__.py create mode 100644 erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js create mode 100644 erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json create mode 100644 erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py create mode 100644 erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 3ddea15ed1d..84c69a6b97d 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -38,14 +38,14 @@ def handle_end_call(**kwargs): @frappe.whitelist(allow_guest=True) def handle_missed_call(**kwargs): status = "" - CallType = kwargs.get("CallType") - DialCallStatus = kwargs.get("DialCallStatus") + call_type = kwargs.get("CallType") + dial_call_status = kwargs.get("DialCallStatus") - if CallType == "incomplete" and DialCallStatus == "no-answer": + if call_type == "incomplete" and dial_call_status == "no-answer": status = 'No Answer' - elif CallType == "client-hangup" and DialCallStatus == "canceled": + elif call_type == "client-hangup" and dial_call_status == "canceled": status = 'Canceled' - elif CallType == "incomplete" and DialCallStatus == "failed": + elif call_type == "incomplete" and dial_call_status == "failed": status = 'Failed' update_call_log(kwargs, status) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index b7d3485b657..2dbe999e05b 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -141,11 +141,10 @@ class CallPopup { 'fieldtype': 'Section Break', 'hide_border': 1, }, { - 'fieldname': 'type_of_call', - 'label': 'Type Of Call', - 'fieldtype': 'Select', - 'options': '\nFrappe Cloud Queries/Plan\nEnterprise Plans\nPartnership\nSupport\nBilling/Renewal\nOpen source / Junk', - 'default': 'Frappe Cloud Queries/Plan', + 'fieldname': 'call_type', + 'label': 'Call Type', + 'fieldtype': 'Link', + 'options': 'Telephony Call Type', }, { 'fieldtype': 'Section Break', 'hide_border': 1, @@ -158,7 +157,7 @@ class CallPopup { 'label': __('Save'), 'click': () => { const call_summary = this.call_details.get_value('call_summary'); - const call_type = this.call_details.get_value('type_of_call'); + const call_type = this.call_details.get_value('call_type'); if (!call_summary) return; frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary_and_call_type', { 'call_log': this.call_log.name, diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index 615e069e72f..cd749e8a018 100644 --- a/erpnext/telephony/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -9,7 +9,7 @@ "id", "from", "to", - "employee_call_directed_to", + "call_received_by", "employee_user_id", "medium", "start_time", @@ -139,13 +139,6 @@ "fieldtype": "Section Break", "label": "Call Details" }, - { - "depends_on": "to", - "fieldname": "employee_call_directed_to", - "fieldtype": "Data", - "label": "Employee Call Directed To", - "read_only": 1 - }, { "fieldname": "employee_user_id", "fieldtype": "Data", @@ -156,12 +149,19 @@ "fieldname": "type_of_call", "fieldtype": "Data", "label": "Type Of Call" + }, + { + "depends_on": "to", + "fieldname": "call_received_by", + "fieldtype": "Data", + "label": "Call Received By", + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-02-25 13:37:48.156501", + "modified": "2022-02-25 14:37:48.575230", "modified_by": "Administrator", "module": "Telephony", "name": "Call Log", diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 768bf5bde22..c35cf751c17 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -37,9 +37,11 @@ class CallLog(Document): # Taking the last 10 digits of the number emp_number_reversed = (self.get("to"))[-1:-11:-1] emp_number = emp_number_reversed[-1::-1] + employee = frappe.get_all("Employee", filters={ + "cell_number": ["like", "%"+emp_number+"%"] + }, fields=["first_name", "middle_name", "last_name", "user_id"]) - employee = frappe.get_all("Employee", filters={"cell_number":["like","%"+emp_number+"%"]}, fields=["first_name", "middle_name", "last_name", "user_id"]) - self.employee_call_directed_to = get_employee_name(employee[0]) + self.call_received_by = get_employee_name(employee[0]) self.employee_user_id = employee[0].get("user_id") or '' def after_insert(self): diff --git a/erpnext/telephony/doctype/telephony_call_type/__init__.py b/erpnext/telephony/doctype/telephony_call_type/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js new file mode 100644 index 00000000000..efba2b86ff5 --- /dev/null +++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Telephony Call Type', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json new file mode 100644 index 00000000000..603709e98f9 --- /dev/null +++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json @@ -0,0 +1,58 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:call_type", + "creation": "2022-02-25 16:13:37.321312", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "call_type", + "amended_from" + ], + "fields": [ + { + "fieldname": "call_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Call Type", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Telephony Call Type", + "print_hide": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2022-02-25 16:14:07.087461", + "modified_by": "Administrator", + "module": "Telephony", + "name": "Telephony Call Type", + "naming_rule": "By fieldname", + "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": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py new file mode 100644 index 00000000000..5abb3180df1 --- /dev/null +++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class TelephonyCallType(Document): + pass diff --git a/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py new file mode 100644 index 00000000000..c16d03e0978 --- /dev/null +++ b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestTelephonyCallType(unittest.TestCase): + pass From b52962751c95ced44557cbe14ea5ff2c507a2bf7 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 28 Feb 2022 12:47:05 +0530 Subject: [PATCH 15/29] fix: linter, sider fixes --- .../telephony/doctype/telephony_call_type/telephony_call_type.py | 1 + .../doctype/telephony_call_type/test_telephony_call_type.py | 1 + 2 files changed, 2 insertions(+) diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py index 5abb3180df1..944ffef36f2 100644 --- a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py +++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class TelephonyCallType(Document): pass diff --git a/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py index c16d03e0978..b3c19c39102 100644 --- a/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py +++ b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestTelephonyCallType(unittest.TestCase): pass From 3f46b2a0ce4663bbb9a3b81d45cfe356acf39aac Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 22 Mar 2022 16:22:09 +0530 Subject: [PATCH 16/29] fix: used get_employees_with_number, strip_number methods --- erpnext/hr/doctype/employee/employee.json | 5 +-- .../telephony/doctype/call_log/call_log.py | 33 ++++++++----------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index ed45e5288c6..0b49e98c8a7 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -4,7 +4,7 @@ "allow_import": 1, "allow_rename": 1, "autoname": "naming_series:", - "creation": "2013-03-07 09:04:18", + "creation": "2022-02-21 11:54:09.632218", "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, @@ -813,11 +813,12 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2022-07-18 20:03:43.188705", + "modified": "2022-08-20 13:44:37.088519", "modified_by": "Administrator", "module": "HR", "name": "Employee", "name_case": "Title Case", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index c35cf751c17..f6690c41de5 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -35,14 +35,11 @@ class CallLog(Document): # Add Employee Name if self.is_incoming_call(): # Taking the last 10 digits of the number - emp_number_reversed = (self.get("to"))[-1:-11:-1] - emp_number = emp_number_reversed[-1::-1] - employee = frappe.get_all("Employee", filters={ - "cell_number": ["like", "%"+emp_number+"%"] - }, fields=["first_name", "middle_name", "last_name", "user_id"]) + employee_number = strip_number(self.get('to')) + employee = get_employees_with_number(self.get("to")) - self.call_received_by = get_employee_name(employee[0]) - self.employee_user_id = employee[0].get("user_id") or '' + self.call_received_by = employee[0].get("name") + self.employee_user_id = employee[0].get("user_id") def after_insert(self): self.trigger_call_popup() @@ -77,7 +74,8 @@ class CallLog(Document): def trigger_call_popup(self): if self.is_incoming_call(): scheduled_employees = get_scheduled_employees_for_popup(self.medium) - employee_emails = get_employees_with_number(self.to) + employees = get_employees_with_number(self.to) + employee_emails = [employee.get("user_id") for employee in employees] # check if employees with matched number are scheduled to receive popup emails = set(scheduled_employees).intersection(employee_emails) @@ -117,20 +115,17 @@ def get_employees_with_number(number): if not number: return [] - employee_emails = frappe.cache().hget("employees_with_number", number) - if employee_emails: - return employee_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 - employees = frappe.get_all( - "Employee", - filters={"cell_number": ["like", "%{}%".format(number)], "user_id": ["!=", ""]}, - fields=["user_id"], - ) + employee_doc_name_and_emails = frappe.get_all('Employee', filters={ + 'cell_number': ['like', '%{}%'.format(number)], + 'user_id': ['!=', ''] + }, fields=['name', 'user_id']) - employee_emails = [employee.user_id for employee in employees] - frappe.cache().hset("employees_with_number", number, employee_emails) + frappe.cache().hset('employees_with_number', number, employee_doc_name_and_emails) - return employee_emails + return employee_doc_name_and_emails def link_existing_conversations(doc, state): From aa8a063103cde202120f1fa0c07d178dcc464ac6 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Sat, 2 Apr 2022 17:16:56 +0530 Subject: [PATCH 17/29] fix: added tests --- .../telephony/doctype/call_log/call_log.py | 1 - erpnext/tests/test_exotel.py | 199 ++++++++++++++++++ 2 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 erpnext/tests/test_exotel.py diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index f6690c41de5..18d354a1895 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -35,7 +35,6 @@ class CallLog(Document): # Add Employee Name if self.is_incoming_call(): # Taking the last 10 digits of the number - employee_number = strip_number(self.get('to')) employee = get_employees_with_number(self.get("to")) self.call_received_by = employee[0].get("name") diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py new file mode 100644 index 00000000000..91e8f50c3cb --- /dev/null +++ b/erpnext/tests/test_exotel.py @@ -0,0 +1,199 @@ +import os +import time +import unittest + +import frappe +import requests +from frappe.contacts.doctype.contact.test_contact import create_contact + +from erpnext.hr.doctype.employee.test_employee import make_employee + + +class TestExotel(unittest.TestCase): + def setUp(self): + make_employee("test_employee_exotel@company.com", cell_number="9999999999") + phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}] + create_contact("Test Contact", "Mr", phones=phones) + + def test_for_successful_call(self): + data = { + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "Created": "Wed, 23 Feb 2022 12:31:59", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-23 12:32:02", + "DialWhomNumber": "09999999999", + "Status": "busy", + "EventType": "Dial", + "AgentEmail": "test_employee_exotel@company.com", + } + end_call_data = { + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Wed, 23 Feb 2022 12:31:59", + "DialCallDuration": "17", + "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/exotelrecordings/erpnext/23c162077629863c1a2d7f29263a162n.mp3", + "StartTime": "2022-02-23 12:31:58", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "completed", + "CallType": "completed", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", + "CurrentTime": "2022-02-23 12:32:25", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "10", + "CallerId": "09999999980", + "CauseCode": "NORMAL_CLEARING", + "Cause": "16", + } + ], + } + api_method = "handle_incoming_call" + end_call_api_method = "handle_end_call" + emulate_api_call(data, api_method, end_call_data, end_call_api_method) + + frappe.reload_doctype("Call Log") + call_log = frappe.get_doc( + "Call Log", {"from": "09999999991", "to": "09999999999", "status": "Completed"} + ) + + self.assertEqual(call_log.get("from"), "09999999991") + self.assertEqual(call_log.get("to"), "09999999999") + self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("status"), "Completed") + + def test_for_disconnected_call(self): + data = { + "CallSid": "d96421addce69e24bdc7ce5880d1162l", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:58:12", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:58:12", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "canceled", + "CallType": "client-hangup", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:58:47", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], + } + api_method = "handle_missed_call" + emulate_api_call(data, api_method) + + frappe.reload_doctype("Call Log") + call_log = frappe.get_doc( + "Call Log", {"from": "09999999991", "to": "09999999999", "status": "Canceled"} + ) + + self.assertEqual(call_log.get("from"), "09999999991") + self.assertEqual(call_log.get("to"), "09999999999") + self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("status"), "Canceled") + + def test_for_call_not_answered(self): + data = { + "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:47:02", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:47:02", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "no-answer", + "CallType": "incomplete", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:47:40", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], + } + api_method = "handle_missed_call" + emulate_api_call(data, api_method) + + frappe.reload_doctype("Call Log") + call_log = frappe.get_doc( + "Call Log", {"from": "09999999991", "to": "09999999999", "status": "No Answer"} + ) + + self.assertEqual(call_log.get("from"), "09999999991") + self.assertEqual(call_log.get("to"), "09999999999") + self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("status"), "No Answer") + + def tearDown(self): + frappe.db.rollback() + + +def emulate_api_call(data, api_method, end_call_data=None, end_call_api_method=None): + # Build URL + port = frappe.get_site_config().webserver_port or "8000" + + if os.environ.get("CI"): + host = "localhost" + else: + host = frappe.local.site + + url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}".format( + site=host, port=port, api_method=api_method + ) + + if end_call_data: + end_call_url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{end_call_api_method}".format( + site=host, port=port, end_call_api_method=end_call_api_method + ) + + requests.post(url=url, data=data) + time.sleep(3) + + if end_call_data: + requests.post(url=end_call_url, data=end_call_data) + time.sleep(3) + + return From 06b1bcbf5959d775f3b9e1ce2304a63a47cb835b Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 07:23:08 +0530 Subject: [PATCH 18/29] test: Refactor exotel test setup - Remove unnecessary code - Move test data to separate file - Make proper test assertions --- erpnext/tests/exotel_test_data.py | 114 +++++++++++++++++++ erpnext/tests/test_exotel.py | 179 +++++------------------------- 2 files changed, 140 insertions(+), 153 deletions(-) create mode 100644 erpnext/tests/exotel_test_data.py diff --git a/erpnext/tests/exotel_test_data.py b/erpnext/tests/exotel_test_data.py new file mode 100644 index 00000000000..e2ae7f776ea --- /dev/null +++ b/erpnext/tests/exotel_test_data.py @@ -0,0 +1,114 @@ +import frappe + +call_initiation_data = frappe._dict({ + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "Created": "Wed, 23 Feb 2022 12:31:59", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-23 12:32:02", + "DialWhomNumber": "09999999999", + "Status": "busy", + "EventType": "Dial", + "AgentEmail": "test_employee_exotel@company.com", +}) + +call_end_data = frappe._dict({ + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Wed, 23 Feb 2022 12:31:59", + "DialCallDuration": "17", + "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3", + "StartTime": "2022-02-23 12:31:58", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "completed", + "CallType": "completed", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", + "CurrentTime": "2022-02-23 12:32:25", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "10", + "CallerId": "09999999980", + "CauseCode": "NORMAL_CLEARING", + "Cause": "16", + } + ], +}) + +call_disconnected_data = frappe._dict({ + "CallSid": "d96421addce69e24bdc7ce5880d1162l", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:58:12", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:58:12", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "canceled", + "CallType": "client-hangup", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:58:47", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], +}) + +call_not_answered_data = frappe._dict({ + "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:47:02", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:47:02", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "no-answer", + "CallType": "incomplete", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:47:40", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], +}) \ No newline at end of file diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index 91e8f50c3cb..d2e317ac6c6 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -16,154 +16,42 @@ class TestExotel(unittest.TestCase): create_contact("Test Contact", "Mr", phones=phones) def test_for_successful_call(self): - data = { - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "Created": "Wed, 23 Feb 2022 12:31:59", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-23 12:32:02", - "DialWhomNumber": "09999999999", - "Status": "busy", - "EventType": "Dial", - "AgentEmail": "test_employee_exotel@company.com", - } - end_call_data = { - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Wed, 23 Feb 2022 12:31:59", - "DialCallDuration": "17", - "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/exotelrecordings/erpnext/23c162077629863c1a2d7f29263a162n.mp3", - "StartTime": "2022-02-23 12:31:58", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "completed", - "CallType": "completed", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", - "CurrentTime": "2022-02-23 12:32:25", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "10", - "CallerId": "09999999980", - "CauseCode": "NORMAL_CLEARING", - "Cause": "16", - } - ], - } + from .exotel_test_data import call_initiation_data, call_end_data api_method = "handle_incoming_call" end_call_api_method = "handle_end_call" - emulate_api_call(data, api_method, end_call_data, end_call_api_method) - frappe.reload_doctype("Call Log") - call_log = frappe.get_doc( - "Call Log", {"from": "09999999991", "to": "09999999999", "status": "Completed"} - ) + emulate_api_call(call_initiation_data, api_method) + emulate_api_call(call_end_data, end_call_api_method) self.assertEqual(call_log.get("from"), "09999999991") self.assertEqual(call_log.get("to"), "09999999999") + call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid) + + self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom) + self.assertEqual(call_log.get("to"), call_initiation_data.DialWhomNumber) self.assertEqual(call_log.get("call_received_by"), "EMP-00001") self.assertEqual(call_log.get("status"), "Completed") def test_for_disconnected_call(self): - data = { - "CallSid": "d96421addce69e24bdc7ce5880d1162l", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:58:12", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:58:12", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "canceled", - "CallType": "client-hangup", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:58:47", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], - } + from .exotel_test_data import call_disconnected_data api_method = "handle_missed_call" - emulate_api_call(data, api_method) + emulate_api_call(call_disconnected_data, api_method) + call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid) - frappe.reload_doctype("Call Log") - call_log = frappe.get_doc( - "Call Log", {"from": "09999999991", "to": "09999999999", "status": "Canceled"} - ) - - self.assertEqual(call_log.get("from"), "09999999991") - self.assertEqual(call_log.get("to"), "09999999999") + self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom) + self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber) self.assertEqual(call_log.get("call_received_by"), "EMP-00001") self.assertEqual(call_log.get("status"), "Canceled") def test_for_call_not_answered(self): - data = { - "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:47:02", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:47:02", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "no-answer", - "CallType": "incomplete", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:47:40", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], - } + from .exotel_test_data import call_not_answered_data api_method = "handle_missed_call" - emulate_api_call(data, api_method) + emulate_api_call(call_not_answered_data, api_method) - frappe.reload_doctype("Call Log") - call_log = frappe.get_doc( - "Call Log", {"from": "09999999991", "to": "09999999999", "status": "No Answer"} - ) + call_log = frappe.get_doc("Call Log", call_not_answered_data.CallSid) - self.assertEqual(call_log.get("from"), "09999999991") - self.assertEqual(call_log.get("to"), "09999999999") + self.assertEqual(call_log.get("from"), call_not_answered_data.CallFrom) + self.assertEqual(call_log.get("to"), call_not_answered_data.DialWhomNumber) self.assertEqual(call_log.get("call_received_by"), "EMP-00001") self.assertEqual(call_log.get("status"), "No Answer") @@ -171,29 +59,14 @@ class TestExotel(unittest.TestCase): frappe.db.rollback() -def emulate_api_call(data, api_method, end_call_data=None, end_call_api_method=None): +def emulate_api_call(data, api_method): # Build URL + url = get_exotel_handler_endpoint(api_method) + res = requests.post(url=url, data=frappe.as_json(data)) + res.raise_for_status() + time.sleep(1) + +def get_exotel_handler_endpoint(method): + site = "localhost" if os.environ.get("CI") else frappe.local.site port = frappe.get_site_config().webserver_port or "8000" - - if os.environ.get("CI"): - host = "localhost" - else: - host = frappe.local.site - - url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}".format( - site=host, port=port, api_method=api_method - ) - - if end_call_data: - end_call_url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{end_call_api_method}".format( - site=host, port=port, end_call_api_method=end_call_api_method - ) - - requests.post(url=url, data=data) - time.sleep(3) - - if end_call_data: - requests.post(url=end_call_url, data=end_call_data) - time.sleep(3) - - return + return f"http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{method}" \ No newline at end of file From f5b967a9478b2baf2a107921fa1f2679a79bc2c5 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 07:28:41 +0530 Subject: [PATCH 19/29] chore: Remove unused code - and simplify get_call_log --- erpnext/erpnext_integrations/exotel_integration.py | 13 +++---------- erpnext/telephony/doctype/call_log/call_log.py | 7 ------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 84c69a6b97d..a8a1aa88a2e 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -68,16 +68,9 @@ def update_call_log(call_payload, status="Ringing", call_log=None): 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) + call_log_id = call_payload.get("CallSid") + if frappe.db.exists("Call Log", call_log_id): + return frappe.get_doc("Call Log", call_log_id) def create_call_log(call_payload): diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 18d354a1895..0daecc05432 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -94,13 +94,6 @@ class CallLog(Document): for email in emails: 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']: - if 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): From baab96d02ce2e52ebb74df9a1070b86eb1130cf1 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 07:38:15 +0530 Subject: [PATCH 20/29] fix: Handle exception where no employee is returned --- erpnext/telephony/doctype/call_log/call_log.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 0daecc05432..3c081bb9065 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -35,10 +35,10 @@ class CallLog(Document): # Add Employee Name if self.is_incoming_call(): # Taking the last 10 digits of the number - employee = get_employees_with_number(self.get("to")) - - self.call_received_by = employee[0].get("name") - self.employee_user_id = employee[0].get("user_id") + employees = get_employees_with_number(self.get("to")) + if employees: + self.call_received_by = employees[0].get("name") + self.employee_user_id = employees[0].get("user_id") def after_insert(self): self.trigger_call_popup() From ca48b9de54e684f65ba9e6b6c736b9a7f81b9101 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 09:31:15 +0530 Subject: [PATCH 21/29] test: Fix erroneous code --- erpnext/tests/exotel_test_data.py | 226 ++++++++++++++++-------------- erpnext/tests/test_exotel.py | 12 +- 2 files changed, 124 insertions(+), 114 deletions(-) diff --git a/erpnext/tests/exotel_test_data.py b/erpnext/tests/exotel_test_data.py index e2ae7f776ea..3ad2575c23d 100644 --- a/erpnext/tests/exotel_test_data.py +++ b/erpnext/tests/exotel_test_data.py @@ -1,114 +1,122 @@ import frappe -call_initiation_data = frappe._dict({ - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "Created": "Wed, 23 Feb 2022 12:31:59", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-23 12:32:02", - "DialWhomNumber": "09999999999", - "Status": "busy", - "EventType": "Dial", - "AgentEmail": "test_employee_exotel@company.com", -}) +call_initiation_data = frappe._dict( + { + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "Created": "Wed, 23 Feb 2022 12:31:59", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-23 12:32:02", + "DialWhomNumber": "09999999999", + "Status": "busy", + "EventType": "Dial", + "AgentEmail": "test_employee_exotel@company.com", + } +) -call_end_data = frappe._dict({ - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Wed, 23 Feb 2022 12:31:59", - "DialCallDuration": "17", - "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3", - "StartTime": "2022-02-23 12:31:58", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "completed", - "CallType": "completed", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", - "CurrentTime": "2022-02-23 12:32:25", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "10", - "CallerId": "09999999980", - "CauseCode": "NORMAL_CLEARING", - "Cause": "16", - } - ], -}) +call_end_data = frappe._dict( + { + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Wed, 23 Feb 2022 12:31:59", + "DialCallDuration": "17", + "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3", + "StartTime": "2022-02-23 12:31:58", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "completed", + "CallType": "completed", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", + "CurrentTime": "2022-02-23 12:32:25", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "10", + "CallerId": "09999999980", + "CauseCode": "NORMAL_CLEARING", + "Cause": "16", + } + ], + } +) -call_disconnected_data = frappe._dict({ - "CallSid": "d96421addce69e24bdc7ce5880d1162l", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:58:12", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:58:12", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "canceled", - "CallType": "client-hangup", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:58:47", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], -}) +call_disconnected_data = frappe._dict( + { + "CallSid": "d96421addce69e24bdc7ce5880d1162l", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:58:12", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:58:12", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "canceled", + "CallType": "client-hangup", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:58:47", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], + } +) -call_not_answered_data = frappe._dict({ - "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:47:02", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:47:02", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "no-answer", - "CallType": "incomplete", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:47:40", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], -}) \ No newline at end of file +call_not_answered_data = frappe._dict( + { + "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:47:02", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:47:02", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "no-answer", + "CallType": "incomplete", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:47:40", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], + } +) diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index d2e317ac6c6..a5dc7dd2118 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -16,15 +16,14 @@ class TestExotel(unittest.TestCase): create_contact("Test Contact", "Mr", phones=phones) def test_for_successful_call(self): - from .exotel_test_data import call_initiation_data, call_end_data + from .exotel_test_data import call_end_data, call_initiation_data + api_method = "handle_incoming_call" end_call_api_method = "handle_end_call" - emulate_api_call(call_initiation_data, api_method) emulate_api_call(call_end_data, end_call_api_method) - self.assertEqual(call_log.get("from"), "09999999991") - self.assertEqual(call_log.get("to"), "09999999999") + call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid) self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom) @@ -34,6 +33,7 @@ class TestExotel(unittest.TestCase): def test_for_disconnected_call(self): from .exotel_test_data import call_disconnected_data + api_method = "handle_missed_call" emulate_api_call(call_disconnected_data, api_method) call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid) @@ -45,6 +45,7 @@ class TestExotel(unittest.TestCase): def test_for_call_not_answered(self): from .exotel_test_data import call_not_answered_data + api_method = "handle_missed_call" emulate_api_call(call_not_answered_data, api_method) @@ -66,7 +67,8 @@ def emulate_api_call(data, api_method): res.raise_for_status() time.sleep(1) + def get_exotel_handler_endpoint(method): site = "localhost" if os.environ.get("CI") else frappe.local.site port = frappe.get_site_config().webserver_port or "8000" - return f"http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{method}" \ No newline at end of file + return f"http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{method}" From 4a61840e39d673d8e2fdf33588c0424b21398b83 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 11:15:29 +0530 Subject: [PATCH 22/29] test: Use FrappeAPITestCase to track coverage --- erpnext/tests/test_exotel.py | 63 +++++++++++++++++------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index a5dc7dd2118..68c70ea3444 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -1,19 +1,20 @@ -import os -import time -import unittest - import frappe -import requests from frappe.contacts.doctype.contact.test_contact import create_contact +from frappe.tests.test_api import FrappeAPITestCase from erpnext.hr.doctype.employee.test_employee import make_employee -class TestExotel(unittest.TestCase): - def setUp(self): - make_employee("test_employee_exotel@company.com", cell_number="9999999999") +class TestExotel(FrappeAPITestCase): + @classmethod + def setUpClass(cls): + cls.CURRENT_DB_CONNECTION = frappe.db + cls.test_employee_name = make_employee( + user="test_employee_exotel@company.com", cell_number="9999999999" + ) phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}] - create_contact("Test Contact", "Mr", phones=phones) + create_contact(name="Test Contact", salutation="Mr", phones=phones) + frappe.db.commit() def test_for_successful_call(self): from .exotel_test_data import call_end_data, call_initiation_data @@ -21,54 +22,48 @@ class TestExotel(unittest.TestCase): api_method = "handle_incoming_call" end_call_api_method = "handle_end_call" - emulate_api_call(call_initiation_data, api_method) - emulate_api_call(call_end_data, end_call_api_method) - + self.emulate_api_call_from_exotel(api_method, call_initiation_data) + self.emulate_api_call_from_exotel(end_call_api_method, call_end_data) call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid) self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom) self.assertEqual(call_log.get("to"), call_initiation_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) self.assertEqual(call_log.get("status"), "Completed") def test_for_disconnected_call(self): from .exotel_test_data import call_disconnected_data api_method = "handle_missed_call" - emulate_api_call(call_disconnected_data, api_method) + self.emulate_api_call_from_exotel(api_method, call_disconnected_data) call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid) self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom) self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) self.assertEqual(call_log.get("status"), "Canceled") def test_for_call_not_answered(self): from .exotel_test_data import call_not_answered_data api_method = "handle_missed_call" - emulate_api_call(call_not_answered_data, api_method) - + self.emulate_api_call_from_exotel(api_method, call_not_answered_data) call_log = frappe.get_doc("Call Log", call_not_answered_data.CallSid) - self.assertEqual(call_log.get("from"), call_not_answered_data.CallFrom) self.assertEqual(call_log.get("to"), call_not_answered_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) self.assertEqual(call_log.get("status"), "No Answer") - def tearDown(self): - frappe.db.rollback() + def emulate_api_call_from_exotel(self, api_method, data): + self.post( + f"/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}", + data=frappe.as_json(data), + content_type="application/json", + as_tuple=True, + ) + # restart db connection to get latest data + frappe.connect() - -def emulate_api_call(data, api_method): - # Build URL - url = get_exotel_handler_endpoint(api_method) - res = requests.post(url=url, data=frappe.as_json(data)) - res.raise_for_status() - time.sleep(1) - - -def get_exotel_handler_endpoint(method): - site = "localhost" if os.environ.get("CI") else frappe.local.site - port = frappe.get_site_config().webserver_port or "8000" - return f"http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{method}" + @classmethod + def tearDownClass(cls): + frappe.db = cls.CURRENT_DB_CONNECTION From 5f4760985c69d6076f6b037b349bc42915792abf Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 20:21:36 +0530 Subject: [PATCH 23/29] test: Fix used frappe._dict to avoid AttributeError down the line ref: https://github.com/frappe/erpnext/runs/5816574721?check_suite_focus=true#step:12:880 --- .../doctype/quality_procedure/test_quality_procedure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py index daf7a694a35..a26ce2383d1 100644 --- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py @@ -19,7 +19,7 @@ class TestQualityProcedure(unittest.TestCase): ) ).insert() - frappe.form_dict = dict( + frappe.form_dict = frappe._dict( doctype="Quality Procedure", quality_procedure_name="Test Child 1", parent_quality_procedure=procedure.name, From b495969eb87c847e37e7f5483281edf9d6dd1f71 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 8 Apr 2022 21:33:29 +0530 Subject: [PATCH 24/29] test: Clean up form_dict To avoid failures like https://github.com/frappe/erpnext/runs/5887687369?check_suite_focus=true#step:12:783 --- erpnext/tests/test_exotel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index 68c70ea3444..67bc5bdbb8e 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -8,6 +8,7 @@ from erpnext.hr.doctype.employee.test_employee import make_employee class TestExotel(FrappeAPITestCase): @classmethod def setUpClass(cls): + frappe.form_dict = frappe._dict() cls.CURRENT_DB_CONNECTION = frappe.db cls.test_employee_name = make_employee( user="test_employee_exotel@company.com", cell_number="9999999999" @@ -37,7 +38,6 @@ class TestExotel(FrappeAPITestCase): api_method = "handle_missed_call" self.emulate_api_call_from_exotel(api_method, call_disconnected_data) call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid) - self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom) self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber) self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) From 4df197a2821848c1629c86d06f6a5dbfa5c80ed3 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 13 Apr 2022 19:10:19 +0530 Subject: [PATCH 25/29] test: Do not overwrite frappe.form_dict to retain proxy reference Set frappe.local.form_dict instead --- .../doctype/request_for_quotation/test_request_for_quotation.py | 1 - .../doctype/quality_procedure/test_quality_procedure.py | 2 +- erpnext/tests/test_exotel.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index dcdba095fbf..064b806e953 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -65,7 +65,6 @@ class TestRequestforQuotation(FrappeTestCase): ) sq.submit() - frappe.form_dict = frappe.local("form_dict") frappe.form_dict.name = rfq.name self.assertEqual(check_supplier_has_docname_access(supplier_wt_appos[0].get("supplier")), True) diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py index a26ce2383d1..04e82112142 100644 --- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py @@ -19,7 +19,7 @@ class TestQualityProcedure(unittest.TestCase): ) ).insert() - frappe.form_dict = frappe._dict( + frappe.local.form_dict = frappe._dict( doctype="Quality Procedure", quality_procedure_name="Test Child 1", parent_quality_procedure=procedure.name, diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index 67bc5bdbb8e..bb916a8092b 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -8,7 +8,6 @@ from erpnext.hr.doctype.employee.test_employee import make_employee class TestExotel(FrappeAPITestCase): @classmethod def setUpClass(cls): - frappe.form_dict = frappe._dict() cls.CURRENT_DB_CONNECTION = frappe.db cls.test_employee_name = make_employee( user="test_employee_exotel@company.com", cell_number="9999999999" From 9263d5ff4d934d1913f4391c156e85af070e2fe4 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 13 Apr 2022 20:12:08 +0530 Subject: [PATCH 26/29] fix: Update received_by if "to" is changed --- erpnext/telephony/doctype/call_log/call_log.py | 14 +++++++++----- erpnext/tests/test_exotel.py | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 3c081bb9065..e831a288943 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -34,11 +34,7 @@ class CallLog(Document): # Add Employee Name if self.is_incoming_call(): - # Taking the last 10 digits of the number - employees = get_employees_with_number(self.get("to")) - if employees: - self.call_received_by = employees[0].get("name") - self.employee_user_id = employees[0].get("user_id") + self.update_received_by() def after_insert(self): self.trigger_call_popup() @@ -57,6 +53,9 @@ class CallLog(Document): if not doc_before_save: return + if self.is_incoming_call() and self.has_value_changed("to"): + self.update_received_by() + if _is_call_missed(doc_before_save, self): frappe.publish_realtime("call_{id}_missed".format(id=self.id), self) self.trigger_call_popup() @@ -94,6 +93,11 @@ class CallLog(Document): for email in emails: frappe.publish_realtime("show_call_popup", self, user=email) + def update_received_by(self): + if employees := get_employees_with_number(self.get("to")): + self.call_received_by = employees[0].get("name") + self.employee_user_id = employees[0].get("user_id") + @frappe.whitelist() def add_call_summary_and_call_type(call_log, summary, call_type): diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index bb916a8092b..76bbb3e05ad 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -12,6 +12,7 @@ class TestExotel(FrappeAPITestCase): cls.test_employee_name = make_employee( user="test_employee_exotel@company.com", cell_number="9999999999" ) + frappe.db.set_value("Exotel Settings", "Exotel Settings", "enabled", 1) phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}] create_contact(name="Test Contact", salutation="Mr", phones=phones) frappe.db.commit() From 89f889068e6461c30de663887d03b4c51aa8007d Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Sat, 20 Aug 2022 14:19:15 +0530 Subject: [PATCH 27/29] style: Fix formatting --- .../exotel_integration.py | 6 +++--- .../telephony/doctype/call_log/call_log.py | 21 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index a8a1aa88a2e..522de9ead83 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -42,11 +42,11 @@ def handle_missed_call(**kwargs): dial_call_status = kwargs.get("DialCallStatus") if call_type == "incomplete" and dial_call_status == "no-answer": - status = 'No Answer' + status = "No Answer" elif call_type == "client-hangup" and dial_call_status == "canceled": - status = 'Canceled' + status = "Canceled" elif call_type == "incomplete" and dial_call_status == "failed": - status = 'Failed' + status = "Failed" update_call_log(kwargs, status) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index e831a288943..89cf0153846 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -101,25 +101,28 @@ class CallLog(Document): @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')) + '

' + summary) + doc.add_comment("Comment", frappe.bold(_("Call Summary")) + "

" + summary) + def get_employees_with_number(number): number = strip_number(number) 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 From 23a441252b33a018b77ac89162cfb4cae4fd287d Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 22 Aug 2022 09:49:15 +0530 Subject: [PATCH 28/29] fix: Replace walrus operator --- erpnext/telephony/doctype/call_log/call_log.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 89cf0153846..8d6867dc154 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -94,7 +94,8 @@ class CallLog(Document): frappe.publish_realtime("show_call_popup", self, user=email) def update_received_by(self): - if employees := get_employees_with_number(self.get("to")): + employees = get_employees_with_number(self.get("to")) + if employees: self.call_received_by = employees[0].get("name") self.employee_user_id = employees[0].get("user_id") From 154adcbb58a940b8a3bc20bb85c4c7d3621c594a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 23 Aug 2022 16:10:27 +0530 Subject: [PATCH 29/29] chore: add Work Order test dependencies (backport #31936) (#31938) chore: add Work Order test dependencies (#31936) (cherry picked from commit fe73d55f702163386e413b61dd504f93a8e8479c) Co-authored-by: HENRY Florian --- erpnext/manufacturing/doctype/work_order/test_work_order.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index c88e91e4221..1e629b2fbfa 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -23,6 +23,8 @@ from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.utils import get_bin +test_dependencies = ["BOM"] + class TestWorkOrder(FrappeTestCase): def setUp(self):