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 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: 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: 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/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 1f5df67e197..522de9ead83 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -37,11 +37,26 @@ def handle_end_call(**kwargs): @frappe.whitelist(allow_guest=True) def handle_missed_call(**kwargs): - update_call_log(kwargs, "Missed") + status = "" + call_type = kwargs.get("CallType") + dial_call_status = kwargs.get("DialCallStatus") + + if call_type == "incomplete" and dial_call_status == "no-answer": + status = "No Answer" + elif call_type == "client-hangup" and dial_call_status == "canceled": + status = "Canceled" + elif call_type == "incomplete" and dial_call_status == "failed": + status = "Failed" + + 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") @@ -53,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/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/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%")} 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() 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..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 @@ -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}) @@ -232,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 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): diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index c954f12ac63..2dbe999e05b 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -140,6 +140,14 @@ class CallPopup { }, { 'fieldtype': 'Section Break', 'hide_border': 1, + }, { + 'fieldname': 'call_type', + 'label': 'Call Type', + 'fieldtype': 'Link', + 'options': 'Telephony Call Type', + }, { + 'fieldtype': 'Section Break', + 'hide_border': 1, }, { 'fieldtype': 'Small Text', 'label': __('Call Summary'), @@ -149,10 +157,12 @@ class CallPopup { 'label': __('Save'), 'click': () => { const call_summary = this.call_details.get_value('call_summary'); + 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', { + 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/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py index daf7a694a35..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 = 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/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 diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index 1d6c39edf6e..cd749e8a018 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,8 @@ "id", "from", "to", + "call_received_by", + "employee_user_id", "medium", "start_time", "end_time", @@ -20,6 +22,7 @@ "recording_url", "recording_html", "section_break_11", + "type_of_call", "summary", "section_break_19", "links" @@ -103,7 +106,8 @@ }, { "fieldname": "summary", - "fieldtype": "Small Text" + "fieldtype": "Small Text", + "label": "Summary" }, { "fieldname": "section_break_11", @@ -134,15 +138,34 @@ "fieldname": "call_details_section", "fieldtype": "Section Break", "label": "Call Details" + }, + { + "fieldname": "employee_user_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Employee User Id" + }, + { + "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": "2021-02-08 14:23:28.744844", + "modified": "2022-02-25 14:37:48.575230", "modified_by": "Administrator", "module": "Telephony", "name": "Call Log", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -164,6 +187,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..8d6867dc154 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -32,6 +32,10 @@ class CallLog(Document): if lead: self.add_link(link_type="Lead", link_name=lead) + # Add Employee Name + if self.is_incoming_call(): + self.update_received_by() + def after_insert(self): self.trigger_call_popup() @@ -49,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() @@ -65,7 +72,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) @@ -85,10 +93,18 @@ class CallLog(Document): for email in emails: frappe.publish_realtime("show_call_popup", self, user=email) + def update_received_by(self): + 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") + @frappe.whitelist() -def add_call_summary(call_log, 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) @@ -97,20 +113,19 @@ 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_doc_name_and_emails = frappe.get_all( "Employee", filters={"cell_number": ["like", "%{}%".format(number)], "user_id": ["!=", ""]}, - fields=["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): 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..944ffef36f2 --- /dev/null +++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py @@ -0,0 +1,9 @@ +# 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..b3c19c39102 --- /dev/null +++ b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + + +class TestTelephonyCallType(unittest.TestCase): + pass diff --git a/erpnext/tests/exotel_test_data.py b/erpnext/tests/exotel_test_data.py new file mode 100644 index 00000000000..3ad2575c23d --- /dev/null +++ b/erpnext/tests/exotel_test_data.py @@ -0,0 +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_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", + } + ], + } +) diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py new file mode 100644 index 00000000000..76bbb3e05ad --- /dev/null +++ b/erpnext/tests/test_exotel.py @@ -0,0 +1,69 @@ +import frappe +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(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" + ) + 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() + + def test_for_successful_call(self): + from .exotel_test_data import call_end_data, call_initiation_data + + api_method = "handle_incoming_call" + end_call_api_method = "handle_end_call" + + 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"), 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" + 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) + 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" + 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"), self.test_employee_name) + self.assertEqual(call_log.get("status"), "No Answer") + + 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() + + @classmethod + def tearDownClass(cls): + frappe.db = cls.CURRENT_DB_CONNECTION