From 8d3011832be06f651085994b148022ff635f4e0d Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 23 Feb 2022 12:59:38 +0530 Subject: [PATCH 01/33] 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 167fcb7165..c4f6636239 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -33,10 +33,23 @@ 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 006606c437463c77b8445102ed49fc7dc45dcb65 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 23 Feb 2022 18:45:50 +0530 Subject: [PATCH 02/33] 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 1d6c39edf6..a3dbb02213 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 0c24484bdf..7d86d12cf4 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -33,6 +33,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 fd20713bd764eae7696458d6062b2ecb2d121561 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Fri, 25 Feb 2022 12:01:53 +0530 Subject: [PATCH 03/33] 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 a3dbb02213..c0f022b020 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 7d86d12cf4..787015169b 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -39,8 +39,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() @@ -93,6 +94,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 00b0f10100a2d38e73cd415c7f0839b68cbaa62d Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Fri, 25 Feb 2022 13:38:57 +0530 Subject: [PATCH 04/33] 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 | 4 +++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index c4f6636239..a94596933d 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -41,6 +41,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 c954f12ac6..2addeae0e5 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 c0f022b020..615e069e72 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 787015169b..e55d2906c9 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -103,8 +103,10 @@ def get_employee_name(emp): @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) def get_employees_with_number(number): From a91fb8892954309a292276fb2fc116ed1cf76fa4 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 05/33] 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 2addeae0e5..b7d3485b65 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 5b79496f057c79163fcea8f165914d109cdbc35f Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Fri, 25 Feb 2022 16:52:25 +0530 Subject: [PATCH 06/33] 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 a94596933d..502dbed4f7 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -34,14 +34,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 2addeae0e5..4d69c4ba59 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 615e069e72..cd749e8a01 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 e55d2906c9..7b81a29fc1 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -38,9 +38,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 0000000000..e69de29bb2 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 0000000000..efba2b86ff --- /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 0000000000..603709e98f --- /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 0000000000..5abb3180df --- /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 0000000000..c16d03e097 --- /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 1ab5b7a811674b808d42ab8b7b2ad924531cc247 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 28 Feb 2022 12:47:05 +0530 Subject: [PATCH 07/33] fix: linter, sider fixes --- erpnext/public/js/call_popup/call_popup.js | 2 +- .../doctype/telephony_call_type/telephony_call_type.py | 1 + .../doctype/telephony_call_type/test_telephony_call_type.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 119aad7e6e..2dbe999e05 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -145,7 +145,7 @@ class CallPopup { 'label': 'Call Type', 'fieldtype': 'Link', 'options': 'Telephony Call Type', - },{ + }, { 'fieldtype': 'Section Break', 'hide_border': 1, }, { 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 5abb3180df..944ffef36f 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 c16d03e097..b3c19c3910 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 b19305aa8350c810535c94dc6af7ba0b9e2dde89 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 22 Mar 2022 16:22:09 +0530 Subject: [PATCH 08/33] fix: used get_employees_with_number, strip_number methods --- erpnext/hr/doctype/employee/employee.json | 7 +++-- .../telephony/doctype/call_log/call_log.py | 27 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index d592a9c79e..8a12f3b1d0 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": "2021-06-17 11:31:37.730760", + "modified": "2022-03-22 13:44:37.088519", "modified_by": "Administrator", "module": "HR", "name": "Employee", "name_case": "Title Case", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -857,7 +858,9 @@ ], "search_fields": "employee_name", "show_name_in_global_search": 1, + "show_title_field_in_link": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "employee_name" } \ No newline at end of file diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 7b81a29fc1..f3c941ed9c 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -36,14 +36,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() @@ -78,7 +75,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) @@ -115,18 +113,17 @@ def get_employees_with_number(number): number = strip_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={ + 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): """ From e57e7bb02ca1713ab63259d97a2f879bb5f40991 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Sat, 2 Apr 2022 17:16:56 +0530 Subject: [PATCH 09/33] fix: added tests(fixed) --- .pre-commit-config.yaml | 3 + .../telephony/doctype/call_log/call_log.py | 1 - erpnext/tests/test_exotel.py | 199 ++++++++++++++++++ 3 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 erpnext/tests/test_exotel.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cba6759f61..bc04b772c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,6 +30,9 @@ repos: rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119 hooks: - id: black + additional_dependencies: [ + 'click==8.0.4' + ] - repo: https://github.com/timothycrosley/isort rev: 5.9.1 diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 21fa9c9ab4..7ef16d76fd 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 0000000000..91e8f50c3c --- /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 0e9ebad9c6bc125ffbd0c23403a3601087282453 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 07:23:08 +0530 Subject: [PATCH 10/33] 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 0000000000..e2ae7f776e --- /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 91e8f50c3c..d2e317ac6c 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 39abfae5feb7ccbcdc22d5af75fe6ebe97820ffe Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 07:28:41 +0530 Subject: [PATCH 11/33] chore: Remove unused code - and simplify get_call_log --- erpnext/erpnext_integrations/exotel_integration.py | 13 +++---------- erpnext/telephony/doctype/call_log/call_log.py | 8 -------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 82bcb8d0db..522de9ead8 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 7ef16d76fd..e7fe7abfe9 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -95,14 +95,6 @@ class CallLog(Document): 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): doc = frappe.get_doc("Call Log", call_log) From cfee53eb558b0cc474cdfd61205c06047f1178d0 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 07:38:15 +0530 Subject: [PATCH 12/33] 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 e7fe7abfe9..2092ec284b 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 f4b8573e648e0ee3ab26fd79f700102536788c41 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 09:31:15 +0530 Subject: [PATCH 13/33] 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 e2ae7f776e..3ad2575c23 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 d2e317ac6c..a5dc7dd211 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 40d33b5fecb0a6418fedb595cfddef2387eb881a Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 11:15:29 +0530 Subject: [PATCH 14/33] 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 a5dc7dd211..68c70ea344 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 01909d2e8cb081197fe7c67d9fb049b3930fa6dd Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 20:21:36 +0530 Subject: [PATCH 15/33] 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 daf7a694a3..a26ce2383d 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 7ff5bc9e3d62ffd0e57600f6383eb865e26ee28f Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 8 Apr 2022 21:33:29 +0530 Subject: [PATCH 16/33] 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 68c70ea344..67bc5bdbb8 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 3ce64170dba0ffd0ecbba5c6b2e8161da0ad82de Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 11 Apr 2022 14:35:22 +0530 Subject: [PATCH 17/33] feat: Item-wise provisional accounting for service items --- .../purchase_invoice/purchase_invoice.py | 6 +++-- erpnext/setup/doctype/company/company.js | 3 ++- erpnext/stock/doctype/item/item.js | 11 ++++++++++ .../doctype/item_default/item_default.json | 10 ++++++++- .../purchase_receipt/purchase_receipt.json | 17 +------------- .../purchase_receipt/purchase_receipt.py | 22 +++++++++++-------- .../purchase_receipt_item.json | 22 +++++++++++++++++-- erpnext/stock/get_item_details.py | 5 +++++ 8 files changed, 65 insertions(+), 31 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index e6a46d0676..a5f9e24e15 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -811,7 +811,9 @@ class PurchaseInvoice(BuyingController): if provisional_accounting_for_non_stock_items: if item.purchase_receipt: - provisional_account = self.get_company_default("default_provisional_account") + provisional_account = frappe.db.get_value( + "Purchase Receipt Item", item.pr_detail, "provisional_expense_account" + ) or self.get_company_default("default_provisional_account") purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) if not purchase_receipt_doc: @@ -834,7 +836,7 @@ class PurchaseInvoice(BuyingController): if expense_booked_in_pr: # Intentionally passing purchase invoice item to handle partial billing purchase_receipt_doc.add_provisional_gl_entry( - item, gl_entries, self.posting_date, reverse=1 + item, gl_entries, self.posting_date, provisional_account, reverse=1 ) if not self.is_internal_transfer(): diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index dd185fc663..0de5b2d5a3 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -233,7 +233,8 @@ erpnext.company.setup_queries = function(frm) { ["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}], ["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}], ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}], - ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}] + ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}], + ["default_provisional_account", {"root_type": ["in", ["Liability", "Asset"]]}] ], function(i, v) { erpnext.company.set_custom_query(frm, v); }); diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 23301a6a78..3abb609302 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -377,6 +377,17 @@ $.extend(erpnext.item, { } } + frm.set_query('default_provisional_account', 'item_defaults', (doc, cdt, cdn) => { + let row = locals[cdt][cdn]; + return { + filters: { + "company": row.company, + "root_type": ["in", ["Liability", "Asset"]], + "is_group": 0 + } + } + }) + }, make_dashboard: function(frm) { diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json index bc171604f4..042d398256 100644 --- a/erpnext/stock/doctype/item_default/item_default.json +++ b/erpnext/stock/doctype/item_default/item_default.json @@ -15,6 +15,7 @@ "default_supplier", "column_break_8", "expense_account", + "default_provisional_account", "selling_defaults", "selling_cost_center", "column_break_12", @@ -101,11 +102,17 @@ "fieldtype": "Link", "label": "Default Discount Account", "options": "Account" + }, + { + "fieldname": "default_provisional_account", + "fieldtype": "Link", + "label": "Default Provisional Account", + "options": "Account" } ], "istable": 1, "links": [], - "modified": "2021-07-13 01:26:03.860065", + "modified": "2022-04-10 20:18:54.148195", "modified_by": "Administrator", "module": "Stock", "name": "Item Default", @@ -114,5 +121,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 6e5f6f5b52..19c490d37f 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -106,8 +106,6 @@ "terms", "bill_no", "bill_date", - "accounting_details_section", - "provisional_expense_account", "more_info", "project", "status", @@ -1145,26 +1143,13 @@ "label": "Represents Company", "options": "Company", "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "accounting_details_section", - "fieldtype": "Section Break", - "label": "Accounting Details" - }, - { - "fieldname": "provisional_expense_account", - "fieldtype": "Link", - "hidden": 1, - "label": "Provisional Expense Account", - "options": "Account" } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-03-10 11:40:52.690984", + "modified": "2022-04-10 22:50:37.761362", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 1e1c0b9f7c..d050624b23 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -145,10 +145,13 @@ class PurchaseReceipt(BuyingController): ) ) - if provisional_accounting_for_non_stock_items: - default_provisional_account = self.get_company_default("default_provisional_account") - if not self.provisional_expense_account: - self.provisional_expense_account = default_provisional_account + if not provisional_accounting_for_non_stock_items: + return + + default_provisional_account = self.get_company_default("default_provisional_account") + for item in self.get("items"): + if not item.get("provisional_expense_account"): + item.provisional_expense_account = default_provisional_account def validate_with_previous_doc(self): super(PurchaseReceipt, self).validate_with_previous_doc( @@ -518,9 +521,10 @@ class PurchaseReceipt(BuyingController): + "\n".join(warehouse_with_no_account) ) - def add_provisional_gl_entry(self, item, gl_entries, posting_date, reverse=0): - provisional_expense_account = self.get("provisional_expense_account") - credit_currency = get_account_currency(provisional_expense_account) + def add_provisional_gl_entry( + self, item, gl_entries, posting_date, provisional_account, reverse=0 + ): + credit_currency = get_account_currency(provisional_account) debit_currency = get_account_currency(item.expense_account) expense_account = item.expense_account remarks = self.get("remarks") or _("Accounting Entry for Service") @@ -534,7 +538,7 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry( gl_entries=gl_entries, - account=provisional_expense_account, + account=provisional_account, cost_center=item.cost_center, debit=0.0, credit=multiplication_factor * item.amount, @@ -554,7 +558,7 @@ class PurchaseReceipt(BuyingController): debit=multiplication_factor * item.amount, credit=0.0, remarks=remarks, - against_account=provisional_expense_account, + against_account=provisional_account, account_currency=debit_currency, project=item.project, voucher_detail_no=item.name, diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 03a4201ce5..1c65ac86c9 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -96,7 +96,6 @@ "include_exploded_items", "batch_no", "rejected_serial_no", - "expense_account", "item_tax_rate", "item_weight_details", "weight_per_unit", @@ -107,6 +106,10 @@ "manufacturer", "column_break_16", "manufacturer_part_no", + "accounting_details_section", + "expense_account", + "column_break_102", + "provisional_expense_account", "accounting_dimensions_section", "project", "dimension_col_break", @@ -971,12 +974,27 @@ "label": "Product Bundle", "options": "Product Bundle", "read_only": 1 + }, + { + "fieldname": "provisional_expense_account", + "fieldtype": "Link", + "label": "Provisional Expense Account", + "options": "Account" + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "column_break_102", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-02-01 11:32:27.980524", + "modified": "2022-04-11 13:07:32.061402", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index d3a230e3d8..324ff4f409 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -345,6 +345,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults), "discount_account": get_default_discount_account(args, item_defaults), + "provisional_expense_account": get_provisional_account(args, item_defaults), "cost_center": get_default_cost_center( args, item_defaults, item_group_defaults, brand_defaults ), @@ -699,6 +700,10 @@ def get_default_expense_account(args, item, item_group, brand): ) +def get_provisional_account(args, item): + return item.get("default_provisional_account") or args.default_provisional_account + + def get_default_discount_account(args, item): return item.get("default_discount_account") or args.discount_account From 993c6c0de9c1e3cfc3c4673e3bc16542204022be Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 12 Apr 2022 12:24:47 +0530 Subject: [PATCH 18/33] feat: Ignore permlevel for specific fields --- erpnext/controllers/buying_controller.py | 3 +++ erpnext/controllers/selling_controller.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index eda36868b9..233b476a14 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -22,6 +22,9 @@ class QtyMismatchError(ValidationError): class BuyingController(StockController, Subcontracting): + def __setup__(self): + self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"] + def get_feed(self): if self.get("supplier_name"): return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7877827ac7..19fedb3c38 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -16,6 +16,9 @@ from erpnext.stock.utils import get_incoming_rate class SellingController(StockController): + def __setup__(self): + self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"] + def get_feed(self): return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total) From 2777c5c67c369fc86aa1748fab2326e72581039a Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 12 Apr 2022 14:30:01 +0530 Subject: [PATCH 19/33] fix: Map Production Plan company in subassembly WO created from it --- erpnext/manufacturing/doctype/production_plan/production_plan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 60b32b84b0..6af1342945 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -462,6 +462,7 @@ class ProductionPlan(Document): work_order_data = { "wip_warehouse": default_warehouses.get("wip_warehouse"), "fg_warehouse": default_warehouses.get("fg_warehouse"), + "company": self.get("company"), } self.prepare_data_for_sub_assembly_items(row, work_order_data) From 9a1c560c8248fd0253a3c1ccbceb6f3eee92bcce Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 12 Apr 2022 15:27:08 +0530 Subject: [PATCH 20/33] fix: Do not show disabled dimensions in reports --- .../accounting_dimension/accounting_dimension.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 897151a97b..445378300b 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -205,10 +205,16 @@ def get_doctypes_with_dimensions(): return frappe.get_hooks("accounting_dimension_doctypes") -def get_accounting_dimensions(as_list=True): +def get_accounting_dimensions(as_list=True, filters=None): + + if not filters: + filters = {"disabled": 0} + if frappe.flags.accounting_dimensions is None: frappe.flags.accounting_dimensions = frappe.get_all( - "Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"] + "Accounting Dimension", + fields=["label", "fieldname", "disabled", "document_type"], + filters=filters, ) if as_list: From 8261b2bb4f5c89d9c73e2d5514ca38d353fa5145 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 12 Apr 2022 15:30:49 +0530 Subject: [PATCH 21/33] refactor: trigger generate schedule when any change made in items table (#29874) * refactor: trigger generate schedule when any change made in items table * chore: added serial validation on server side * test: serials updated in schedules after save * fix: schedule not generating after updating some fields * feat: generate_schedule is triggered on_save when items table is changed * test: updated tests to check other field changes on save * chore: removed serial validation function for schedules table and added no_of_visits validation function * test: updated for manually deleted schedele rows * refactor: updated validate_items_table_change to return bool * test: updated test_schedule_with_serials to cover validate_items_table_change * fix: linting --- .../maintenance_schedule.js | 20 ------------- .../maintenance_schedule.py | 22 +++++++++++++- .../test_maintenance_schedule.py | 30 +++++++++++++++++++ 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js index 035290d8f1..5252798ba5 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js @@ -140,26 +140,6 @@ erpnext.maintenance.MaintenanceSchedule = class MaintenanceSchedule extends frap } } - start_date(doc, cdt, cdn) { - this.set_no_of_visits(doc, cdt, cdn); - } - - end_date(doc, cdt, cdn) { - this.set_no_of_visits(doc, cdt, cdn); - } - - periodicity(doc, cdt, cdn) { - this.set_no_of_visits(doc, cdt, cdn); - } - - set_no_of_visits(doc, cdt, cdn) { - var item = frappe.get_doc(cdt, cdn); - let me = this; - if (item.start_date && item.periodicity) { - me.frm.call('validate_end_date_visits'); - - } - } }; extend_cscript(cur_frm.cscript, new erpnext.maintenance.MaintenanceSchedule({frm: cur_frm})); diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 9a23c07106..04c080cc72 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -213,6 +213,26 @@ class MaintenanceSchedule(TransactionBase): if chk: throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order)) + def validate_items_table_change(self): + doc_before_save = self.get_doc_before_save() + if not doc_before_save: + return + for prev_item, item in zip(doc_before_save.items, self.items): + fields = [ + "item_code", + "start_date", + "end_date", + "periodicity", + "sales_person", + "no_of_visits", + "serial_no", + ] + for field in fields: + b_doc = prev_item.as_dict() + doc = item.as_dict() + if cstr(b_doc[field]) != cstr(doc[field]): + return True + def validate_no_of_visits(self): return len(self.schedules) != sum(d.no_of_visits for d in self.items) @@ -221,7 +241,7 @@ class MaintenanceSchedule(TransactionBase): self.validate_maintenance_detail() self.validate_dates_with_periodicity() self.validate_sales_order() - if not self.schedules or self.validate_no_of_visits(): + if not self.schedules or self.validate_items_table_change() or self.validate_no_of_visits(): self.generate_schedule() def on_update(self): diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index a98cd10e32..2268e0f7af 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -123,6 +123,36 @@ class TestMaintenanceSchedule(unittest.TestCase): frappe.db.rollback() + def test_schedule_with_serials(self): + # Checks whether serials are automatically updated when changing in items table. + # Also checks if other fields trigger generate schdeule if changed in items table. + item_code = "_Test Serial Item" + make_serial_item_with_serial(item_code) + ms = make_maintenance_schedule(item_code=item_code, serial_no="TEST001, TEST002") + ms.save() + + # Before Save + self.assertEqual(ms.schedules[0].serial_no, "TEST001, TEST002") + self.assertEqual(ms.schedules[0].sales_person, "Sales Team") + self.assertEqual(len(ms.schedules), 4) + self.assertFalse(ms.validate_items_table_change()) + # After Save + ms.items[0].serial_no = "TEST001" + ms.items[0].sales_person = "_Test Sales Person" + ms.items[0].no_of_visits = 2 + self.assertTrue(ms.validate_items_table_change()) + ms.save() + self.assertEqual(ms.schedules[0].serial_no, "TEST001") + self.assertEqual(ms.schedules[0].sales_person, "_Test Sales Person") + self.assertEqual(len(ms.schedules), 2) + # When user manually deleted a row from schedules table. + ms.schedules.pop() + self.assertEqual(len(ms.schedules), 1) + ms.save() + self.assertEqual(len(ms.schedules), 2) + + frappe.db.rollback() + def make_serial_item_with_serial(item_code): serial_item_doc = create_item(item_code, is_stock_item=1) From 6315acc4507a4e9ff6ea3ec03971b0043e798783 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 13 Apr 2022 11:47:58 +0530 Subject: [PATCH 22/33] fix: Map correct company to PO made via Prod Plan (subcontract) --- erpnext/manufacturing/doctype/production_plan/production_plan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 6af1342945..9ca05b927f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -500,6 +500,7 @@ class ProductionPlan(Document): for supplier, po_list in subcontracted_po.items(): po = frappe.new_doc("Purchase Order") + po.company = self.company po.supplier = supplier po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate() po.is_subcontracted = 1 From 6453fb4cea1a1adc4f9c158594dd4d6919a410a6 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 13 Apr 2022 11:24:19 +0200 Subject: [PATCH 23/33] refactor!: remove DATEV integration (#30584) The DATEV integration follows the trend and moves into a separate app. - the DATEV Integration will be maintained at https://github.com/alyf-de/erpnext_datev (version-14 branch) - the German Compliance and Localization will be maintained at https://github.com/alyf-de/erpnext_germany --- erpnext/patches.txt | 5 +- .../germany_fill_debtor_creditor_number.py | 36 -- .../v13_0/germany_make_custom_fields.py | 20 - .../patches/v14_0/delete_datev_doctypes.py | 13 + .../doctype/datev_settings/__init__.py | 0 .../doctype/datev_settings/datev_settings.js | 8 - .../datev_settings/datev_settings.json | 125 ---- .../doctype/datev_settings/datev_settings.py | 10 - .../datev_settings/test_datev_settings.py | 9 - erpnext/regional/germany/__init__.py | 0 erpnext/regional/germany/setup.py | 35 -- erpnext/regional/germany/utils/__init__.py | 0 .../regional/germany/utils/datev/__init__.py | 0 .../germany/utils/datev/datev_constants.py | 501 --------------- .../regional/germany/utils/datev/datev_csv.py | 184 ------ erpnext/regional/report/datev/__init__.py | 0 erpnext/regional/report/datev/datev.js | 56 -- erpnext/regional/report/datev/datev.json | 22 - erpnext/regional/report/datev/datev.py | 570 ------------------ erpnext/regional/report/datev/test_datev.py | 252 -------- 20 files changed, 15 insertions(+), 1831 deletions(-) delete mode 100644 erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py delete mode 100644 erpnext/patches/v13_0/germany_make_custom_fields.py create mode 100644 erpnext/patches/v14_0/delete_datev_doctypes.py delete mode 100644 erpnext/regional/doctype/datev_settings/__init__.py delete mode 100644 erpnext/regional/doctype/datev_settings/datev_settings.js delete mode 100644 erpnext/regional/doctype/datev_settings/datev_settings.json delete mode 100644 erpnext/regional/doctype/datev_settings/datev_settings.py delete mode 100644 erpnext/regional/doctype/datev_settings/test_datev_settings.py delete mode 100644 erpnext/regional/germany/__init__.py delete mode 100644 erpnext/regional/germany/setup.py delete mode 100644 erpnext/regional/germany/utils/__init__.py delete mode 100644 erpnext/regional/germany/utils/datev/__init__.py delete mode 100644 erpnext/regional/germany/utils/datev/datev_constants.py delete mode 100644 erpnext/regional/germany/utils/datev/datev_csv.py delete mode 100644 erpnext/regional/report/datev/__init__.py delete mode 100644 erpnext/regional/report/datev/datev.js delete mode 100644 erpnext/regional/report/datev/datev.json delete mode 100644 erpnext/regional/report/datev/datev.py delete mode 100644 erpnext/regional/report/datev/test_datev.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6e5ffed779..63b6bb73e8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -262,8 +262,6 @@ erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 #17-01-2022 erpnext.patches.v13_0.update_shipment_status erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting erpnext.patches.v12_0.add_ewaybill_validity_field -erpnext.patches.v13_0.germany_make_custom_fields -erpnext.patches.v13_0.germany_fill_debtor_creditor_number erpnext.patches.v13_0.set_pos_closing_as_failed erpnext.patches.v13_0.rename_stop_to_send_birthday_reminders execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True) @@ -343,6 +341,7 @@ erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022 erpnext.patches.v14_0.delete_agriculture_doctypes +erpnext.patches.v14_0.delete_datev_doctypes erpnext.patches.v14_0.rearrange_company_fields erpnext.patches.v14_0.update_leave_notification_template erpnext.patches.v14_0.restore_einvoice_fields @@ -364,4 +363,4 @@ erpnext.patches.v13_0.add_cost_center_in_loans erpnext.patches.v13_0.set_return_against_in_pos_invoice_references erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances -erpnext.patches.v13_0.create_gst_custom_fields_in_quotation \ No newline at end of file +erpnext.patches.v13_0.create_gst_custom_fields_in_quotation diff --git a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py b/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py deleted file mode 100644 index fc3e68ac67..0000000000 --- a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2019, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe - - -def execute(): - """Move account number into the new custom field debtor_creditor_number. - - German companies used to use a dedicated payable/receivable account for - every party to mimick party accounts in the external accounting software - "DATEV". This is no longer necessary. The reference ID for DATEV will be - stored in a new custom field "debtor_creditor_number". - """ - company_list = frappe.get_all("Company", filters={"country": "Germany"}) - - for company in company_list: - party_account_list = frappe.get_all( - "Party Account", - filters={"company": company.name}, - fields=["name", "account", "debtor_creditor_number"], - ) - for party_account in party_account_list: - if (not party_account.account) or party_account.debtor_creditor_number: - # account empty or debtor_creditor_number already filled - continue - - account_number = frappe.db.get_value("Account", party_account.account, "account_number") - if not account_number: - continue - - frappe.db.set_value( - "Party Account", party_account.name, "debtor_creditor_number", account_number - ) - frappe.db.set_value("Party Account", party_account.name, "account", "") diff --git a/erpnext/patches/v13_0/germany_make_custom_fields.py b/erpnext/patches/v13_0/germany_make_custom_fields.py deleted file mode 100644 index cc358135ac..0000000000 --- a/erpnext/patches/v13_0/germany_make_custom_fields.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2019, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe - -from erpnext.regional.germany.setup import make_custom_fields - - -def execute(): - """Execute the make_custom_fields method for german companies. - - It is usually run once at setup of a new company. Since it's new, run it - once for existing companies as well. - """ - company_list = frappe.get_all("Company", filters={"country": "Germany"}) - if not company_list: - return - - make_custom_fields() diff --git a/erpnext/patches/v14_0/delete_datev_doctypes.py b/erpnext/patches/v14_0/delete_datev_doctypes.py new file mode 100644 index 0000000000..a5de91f9c2 --- /dev/null +++ b/erpnext/patches/v14_0/delete_datev_doctypes.py @@ -0,0 +1,13 @@ +import frappe + + +def execute(): + install_apps = frappe.get_installed_apps() + if "erpnext_datev_uo" in install_apps or "erpnext_datev" in install_apps: + return + + # doctypes + frappe.delete_doc("DocType", "DATEV Settings", ignore_missing=True, force=True) + + # reports + frappe.delete_doc("Report", "DATEV", ignore_missing=True, force=True) diff --git a/erpnext/regional/doctype/datev_settings/__init__.py b/erpnext/regional/doctype/datev_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.js b/erpnext/regional/doctype/datev_settings/datev_settings.js deleted file mode 100644 index 3c365494c4..0000000000 --- a/erpnext/regional/doctype/datev_settings/datev_settings.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('DATEV Settings', { - refresh: function(frm) { - frm.add_custom_button(__('Show Report'), () => frappe.set_route('query-report', 'DATEV'), "fa fa-table"); - } -}); diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json deleted file mode 100644 index f60de4c8af..0000000000 --- a/erpnext/regional/doctype/datev_settings/datev_settings.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "actions": [], - "autoname": "field:client", - "creation": "2019-08-13 23:56:34.259906", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "client", - "client_number", - "column_break_2", - "consultant_number", - "consultant", - "section_break_4", - "account_number_length", - "column_break_6", - "temporary_against_account_number" - ], - "fields": [ - { - "fieldname": "client", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Client", - "options": "Company", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "client_number", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Client ID", - "length": 5, - "reqd": 1 - }, - { - "fieldname": "consultant", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Consultant", - "options": "Supplier" - }, - { - "fieldname": "consultant_number", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Consultant ID", - "length": 7, - "reqd": 1 - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_4", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break" - }, - { - "default": "4", - "fieldname": "account_number_length", - "fieldtype": "Int", - "label": "Account Number Length", - "reqd": 1 - }, - { - "allow_in_quick_entry": 1, - "fieldname": "temporary_against_account_number", - "fieldtype": "Data", - "label": "Temporary Against Account Number", - "reqd": 1 - } - ], - "links": [], - "modified": "2020-11-19 19:00:09.088816", - "modified_by": "Administrator", - "module": "Regional", - "name": "DATEV Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "share": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.py b/erpnext/regional/doctype/datev_settings/datev_settings.py deleted file mode 100644 index 686a93e529..0000000000 --- a/erpnext/regional/doctype/datev_settings/datev_settings.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -# import frappe -from frappe.model.document import Document - - -class DATEVSettings(Document): - pass diff --git a/erpnext/regional/doctype/datev_settings/test_datev_settings.py b/erpnext/regional/doctype/datev_settings/test_datev_settings.py deleted file mode 100644 index ba70eb472f..0000000000 --- a/erpnext/regional/doctype/datev_settings/test_datev_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -import unittest - - -class TestDATEVSettings(unittest.TestCase): - pass diff --git a/erpnext/regional/germany/__init__.py b/erpnext/regional/germany/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/regional/germany/setup.py b/erpnext/regional/germany/setup.py deleted file mode 100644 index b8e66c3ece..0000000000 --- a/erpnext/regional/germany/setup.py +++ /dev/null @@ -1,35 +0,0 @@ -import frappe -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields - - -def setup(company=None, patch=True): - make_custom_fields() - add_custom_roles_for_reports() - - -def make_custom_fields(): - custom_fields = { - "Party Account": [ - dict( - fieldname="debtor_creditor_number", - label="Debtor/Creditor Number", - fieldtype="Data", - insert_after="account", - translatable=0, - ) - ] - } - - create_custom_fields(custom_fields) - - -def add_custom_roles_for_reports(): - """Add Access Control to UAE VAT 201.""" - if not frappe.db.get_value("Custom Role", dict(report="DATEV")): - frappe.get_doc( - dict( - doctype="Custom Role", - report="DATEV", - roles=[dict(role="Accounts User"), dict(role="Accounts Manager")], - ) - ).insert() diff --git a/erpnext/regional/germany/utils/__init__.py b/erpnext/regional/germany/utils/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/regional/germany/utils/datev/__init__.py b/erpnext/regional/germany/utils/datev/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/regional/germany/utils/datev/datev_constants.py b/erpnext/regional/germany/utils/datev/datev_constants.py deleted file mode 100644 index 9524481c11..0000000000 --- a/erpnext/regional/germany/utils/datev/datev_constants.py +++ /dev/null @@ -1,501 +0,0 @@ -"""Constants used in datev.py.""" - -TRANSACTION_COLUMNS = [ - # All possible columns must tbe listed here, because DATEV requires them to - # be present in the CSV. - # --- - # Umsatz - "Umsatz (ohne Soll/Haben-Kz)", - "Soll/Haben-Kennzeichen", - "WKZ Umsatz", - "Kurs", - "Basis-Umsatz", - "WKZ Basis-Umsatz", - # Konto/Gegenkonto - "Konto", - "Gegenkonto (ohne BU-Schlüssel)", - "BU-Schlüssel", - # Datum - "Belegdatum", - # Rechnungs- / Belegnummer - "Belegfeld 1", - # z.B. Fälligkeitsdatum Format: TTMMJJ - "Belegfeld 2", - # Skonto-Betrag / -Abzug (Der Wert 0 ist unzulässig) - "Skonto", - # Beschreibung des Buchungssatzes - "Buchungstext", - # Mahn- / Zahl-Sperre (1 = Postensperre) - "Postensperre", - "Diverse Adressnummer", - "Geschäftspartnerbank", - "Sachverhalt", - # Keine Mahnzinsen - "Zinssperre", - # Link auf den Buchungsbeleg (Programmkürzel + GUID) - "Beleglink", - # Beleginfo - "Beleginfo - Art 1", - "Beleginfo - Inhalt 1", - "Beleginfo - Art 2", - "Beleginfo - Inhalt 2", - "Beleginfo - Art 3", - "Beleginfo - Inhalt 3", - "Beleginfo - Art 4", - "Beleginfo - Inhalt 4", - "Beleginfo - Art 5", - "Beleginfo - Inhalt 5", - "Beleginfo - Art 6", - "Beleginfo - Inhalt 6", - "Beleginfo - Art 7", - "Beleginfo - Inhalt 7", - "Beleginfo - Art 8", - "Beleginfo - Inhalt 8", - # Zuordnung des Geschäftsvorfalls für die Kostenrechnung - "KOST1 - Kostenstelle", - "KOST2 - Kostenstelle", - "KOST-Menge", - # USt-ID-Nummer (Beispiel: DE133546770) - "EU-Mitgliedstaat u. USt-IdNr.", - # Der im EU-Bestimmungsland gültige Steuersatz - "EU-Steuersatz", - # I = Ist-Versteuerung, - # K = keine Umsatzsteuerrechnung - # P = Pauschalierung (z. B. für Land- und Forstwirtschaft), - # S = Soll-Versteuerung - "Abw. Versteuerungsart", - # Sachverhalte gem. § 13b Abs. 1 Satz 1 Nrn. 1.-5. UStG - "Sachverhalt L+L", - # Steuersatz / Funktion zum L+L-Sachverhalt (Beispiel: Wert 190 für 19%) - "Funktionsergänzung L+L", - # Bei Verwendung des BU-Schlüssels 49 für „andere Steuersätze“ muss der - # steuerliche Sachverhalt mitgegeben werden - "BU 49 Hauptfunktionstyp", - "BU 49 Hauptfunktionsnummer", - "BU 49 Funktionsergänzung", - # Zusatzinformationen, besitzen den Charakter eines Notizzettels und können - # frei erfasst werden. - "Zusatzinformation - Art 1", - "Zusatzinformation - Inhalt 1", - "Zusatzinformation - Art 2", - "Zusatzinformation - Inhalt 2", - "Zusatzinformation - Art 3", - "Zusatzinformation - Inhalt 3", - "Zusatzinformation - Art 4", - "Zusatzinformation - Inhalt 4", - "Zusatzinformation - Art 5", - "Zusatzinformation - Inhalt 5", - "Zusatzinformation - Art 6", - "Zusatzinformation - Inhalt 6", - "Zusatzinformation - Art 7", - "Zusatzinformation - Inhalt 7", - "Zusatzinformation - Art 8", - "Zusatzinformation - Inhalt 8", - "Zusatzinformation - Art 9", - "Zusatzinformation - Inhalt 9", - "Zusatzinformation - Art 10", - "Zusatzinformation - Inhalt 10", - "Zusatzinformation - Art 11", - "Zusatzinformation - Inhalt 11", - "Zusatzinformation - Art 12", - "Zusatzinformation - Inhalt 12", - "Zusatzinformation - Art 13", - "Zusatzinformation - Inhalt 13", - "Zusatzinformation - Art 14", - "Zusatzinformation - Inhalt 14", - "Zusatzinformation - Art 15", - "Zusatzinformation - Inhalt 15", - "Zusatzinformation - Art 16", - "Zusatzinformation - Inhalt 16", - "Zusatzinformation - Art 17", - "Zusatzinformation - Inhalt 17", - "Zusatzinformation - Art 18", - "Zusatzinformation - Inhalt 18", - "Zusatzinformation - Art 19", - "Zusatzinformation - Inhalt 19", - "Zusatzinformation - Art 20", - "Zusatzinformation - Inhalt 20", - # Wirkt sich nur bei Sachverhalt mit SKR 14 Land- und Forstwirtschaft aus, - # für andere SKR werden die Felder beim Import / Export überlesen bzw. - # leer exportiert. - "Stück", - "Gewicht", - # 1 = Lastschrift - # 2 = Mahnung - # 3 = Zahlung - "Zahlweise", - "Forderungsart", - # JJJJ - "Veranlagungsjahr", - # TTMMJJJJ - "Zugeordnete Fälligkeit", - # 1 = Einkauf von Waren - # 2 = Erwerb von Roh-Hilfs- und Betriebsstoffen - "Skontotyp", - # Allgemeine Bezeichnung, des Auftrags / Projekts. - "Auftragsnummer", - # AA = Angeforderte Anzahlung / Abschlagsrechnung - # AG = Erhaltene Anzahlung (Geldeingang) - # AV = Erhaltene Anzahlung (Verbindlichkeit) - # SR = Schlussrechnung - # SU = Schlussrechnung (Umbuchung) - # SG = Schlussrechnung (Geldeingang) - # SO = Sonstige - "Buchungstyp", - "USt-Schlüssel (Anzahlungen)", - "EU-Mitgliedstaat (Anzahlungen)", - "Sachverhalt L+L (Anzahlungen)", - "EU-Steuersatz (Anzahlungen)", - "Erlöskonto (Anzahlungen)", - # Wird beim Import durch SV (Stapelverarbeitung) ersetzt. - "Herkunft-Kz", - # Wird von DATEV verwendet. - "Leerfeld", - # Format TTMMJJJJ - "KOST-Datum", - # Vom Zahlungsempfänger individuell vergebenes Kennzeichen eines Mandats - # (z.B. Rechnungs- oder Kundennummer). - "SEPA-Mandatsreferenz", - # 1 = Skontosperre - # 0 = Keine Skontosperre - "Skontosperre", - # Gesellschafter und Sonderbilanzsachverhalt - "Gesellschaftername", - # Amtliche Nummer aus der Feststellungserklärung - "Beteiligtennummer", - "Identifikationsnummer", - "Zeichnernummer", - # Format TTMMJJJJ - "Postensperre bis", - # Gesellschafter und Sonderbilanzsachverhalt - "Bezeichnung SoBil-Sachverhalt", - "Kennzeichen SoBil-Buchung", - # 0 = keine Festschreibung - # 1 = Festschreibung - "Festschreibung", - # Format TTMMJJJJ - "Leistungsdatum", - # Format TTMMJJJJ - "Datum Zuord. Steuerperiode", - # OPOS-Informationen, Format TTMMJJJJ - "Fälligkeit", - # G oder 1 = Generalumkehr - # 0 = keine Generalumkehr - "Generalumkehr (GU)", - # Steuersatz für Steuerschlüssel - "Steuersatz", - # Beispiel: DE für Deutschland - "Land", -] - -DEBTOR_CREDITOR_COLUMNS = [ - # All possible columns must tbe listed here, because DATEV requires them to - # be present in the CSV. - # Columns "Leerfeld" have been replaced with "Leerfeld #" to not confuse pandas - # --- - "Konto", - "Name (Adressatentyp Unternehmen)", - "Unternehmensgegenstand", - "Name (Adressatentyp natürl. Person)", - "Vorname (Adressatentyp natürl. Person)", - "Name (Adressatentyp keine Angabe)", - "Adressatentyp", - "Kurzbezeichnung", - "EU-Land", - "EU-USt-IdNr.", - "Anrede", - "Titel/Akad. Grad", - "Adelstitel", - "Namensvorsatz", - "Adressart", - "Straße", - "Postfach", - "Postleitzahl", - "Ort", - "Land", - "Versandzusatz", - "Adresszusatz", - "Abweichende Anrede", - "Abw. Zustellbezeichnung 1", - "Abw. Zustellbezeichnung 2", - "Kennz. Korrespondenzadresse", - "Adresse gültig von", - "Adresse gültig bis", - "Telefon", - "Bemerkung (Telefon)", - "Telefon Geschäftsleitung", - "Bemerkung (Telefon GL)", - "E-Mail", - "Bemerkung (E-Mail)", - "Internet", - "Bemerkung (Internet)", - "Fax", - "Bemerkung (Fax)", - "Sonstige", - "Bemerkung (Sonstige)", - "Bankleitzahl 1", - "Bankbezeichnung 1", - "Bankkonto-Nummer 1", - "Länderkennzeichen 1", - "IBAN 1", - "Leerfeld 1", - "SWIFT-Code 1", - "Abw. Kontoinhaber 1", - "Kennz. Haupt-Bankverb. 1", - "Bankverb. 1 Gültig von", - "Bankverb. 1 Gültig bis", - "Bankleitzahl 2", - "Bankbezeichnung 2", - "Bankkonto-Nummer 2", - "Länderkennzeichen 2", - "IBAN 2", - "Leerfeld 2", - "SWIFT-Code 2", - "Abw. Kontoinhaber 2", - "Kennz. Haupt-Bankverb. 2", - "Bankverb. 2 gültig von", - "Bankverb. 2 gültig bis", - "Bankleitzahl 3", - "Bankbezeichnung 3", - "Bankkonto-Nummer 3", - "Länderkennzeichen 3", - "IBAN 3", - "Leerfeld 3", - "SWIFT-Code 3", - "Abw. Kontoinhaber 3", - "Kennz. Haupt-Bankverb. 3", - "Bankverb. 3 gültig von", - "Bankverb. 3 gültig bis", - "Bankleitzahl 4", - "Bankbezeichnung 4", - "Bankkonto-Nummer 4", - "Länderkennzeichen 4", - "IBAN 4", - "Leerfeld 4", - "SWIFT-Code 4", - "Abw. Kontoinhaber 4", - "Kennz. Haupt-Bankverb. 4", - "Bankverb. 4 Gültig von", - "Bankverb. 4 Gültig bis", - "Bankleitzahl 5", - "Bankbezeichnung 5", - "Bankkonto-Nummer 5", - "Länderkennzeichen 5", - "IBAN 5", - "Leerfeld 5", - "SWIFT-Code 5", - "Abw. Kontoinhaber 5", - "Kennz. Haupt-Bankverb. 5", - "Bankverb. 5 gültig von", - "Bankverb. 5 gültig bis", - "Leerfeld 6", - "Briefanrede", - "Grußformel", - "Kundennummer", - "Steuernummer", - "Sprache", - "Ansprechpartner", - "Vertreter", - "Sachbearbeiter", - "Diverse-Konto", - "Ausgabeziel", - "Währungssteuerung", - "Kreditlimit (Debitor)", - "Zahlungsbedingung", - "Fälligkeit in Tagen (Debitor)", - "Skonto in Prozent (Debitor)", - "Kreditoren-Ziel 1 (Tage)", - "Kreditoren-Skonto 1 (%)", - "Kreditoren-Ziel 2 (Tage)", - "Kreditoren-Skonto 2 (%)", - "Kreditoren-Ziel 3 Brutto (Tage)", - "Kreditoren-Ziel 4 (Tage)", - "Kreditoren-Skonto 4 (%)", - "Kreditoren-Ziel 5 (Tage)", - "Kreditoren-Skonto 5 (%)", - "Mahnung", - "Kontoauszug", - "Mahntext 1", - "Mahntext 2", - "Mahntext 3", - "Kontoauszugstext", - "Mahnlimit Betrag", - "Mahnlimit %", - "Zinsberechnung", - "Mahnzinssatz 1", - "Mahnzinssatz 2", - "Mahnzinssatz 3", - "Lastschrift", - "Verfahren", - "Mandantenbank", - "Zahlungsträger", - "Indiv. Feld 1", - "Indiv. Feld 2", - "Indiv. Feld 3", - "Indiv. Feld 4", - "Indiv. Feld 5", - "Indiv. Feld 6", - "Indiv. Feld 7", - "Indiv. Feld 8", - "Indiv. Feld 9", - "Indiv. Feld 10", - "Indiv. Feld 11", - "Indiv. Feld 12", - "Indiv. Feld 13", - "Indiv. Feld 14", - "Indiv. Feld 15", - "Abweichende Anrede (Rechnungsadresse)", - "Adressart (Rechnungsadresse)", - "Straße (Rechnungsadresse)", - "Postfach (Rechnungsadresse)", - "Postleitzahl (Rechnungsadresse)", - "Ort (Rechnungsadresse)", - "Land (Rechnungsadresse)", - "Versandzusatz (Rechnungsadresse)", - "Adresszusatz (Rechnungsadresse)", - "Abw. Zustellbezeichnung 1 (Rechnungsadresse)", - "Abw. Zustellbezeichnung 2 (Rechnungsadresse)", - "Adresse Gültig von (Rechnungsadresse)", - "Adresse Gültig bis (Rechnungsadresse)", - "Bankleitzahl 6", - "Bankbezeichnung 6", - "Bankkonto-Nummer 6", - "Länderkennzeichen 6", - "IBAN 6", - "Leerfeld 7", - "SWIFT-Code 6", - "Abw. Kontoinhaber 6", - "Kennz. Haupt-Bankverb. 6", - "Bankverb 6 gültig von", - "Bankverb 6 gültig bis", - "Bankleitzahl 7", - "Bankbezeichnung 7", - "Bankkonto-Nummer 7", - "Länderkennzeichen 7", - "IBAN 7", - "Leerfeld 8", - "SWIFT-Code 7", - "Abw. Kontoinhaber 7", - "Kennz. Haupt-Bankverb. 7", - "Bankverb 7 gültig von", - "Bankverb 7 gültig bis", - "Bankleitzahl 8", - "Bankbezeichnung 8", - "Bankkonto-Nummer 8", - "Länderkennzeichen 8", - "IBAN 8", - "Leerfeld 9", - "SWIFT-Code 8", - "Abw. Kontoinhaber 8", - "Kennz. Haupt-Bankverb. 8", - "Bankverb 8 gültig von", - "Bankverb 8 gültig bis", - "Bankleitzahl 9", - "Bankbezeichnung 9", - "Bankkonto-Nummer 9", - "Länderkennzeichen 9", - "IBAN 9", - "Leerfeld 10", - "SWIFT-Code 9", - "Abw. Kontoinhaber 9", - "Kennz. Haupt-Bankverb. 9", - "Bankverb 9 gültig von", - "Bankverb 9 gültig bis", - "Bankleitzahl 10", - "Bankbezeichnung 10", - "Bankkonto-Nummer 10", - "Länderkennzeichen 10", - "IBAN 10", - "Leerfeld 11", - "SWIFT-Code 10", - "Abw. Kontoinhaber 10", - "Kennz. Haupt-Bankverb. 10", - "Bankverb 10 gültig von", - "Bankverb 10 gültig bis", - "Nummer Fremdsystem", - "Insolvent", - "SEPA-Mandatsreferenz 1", - "SEPA-Mandatsreferenz 2", - "SEPA-Mandatsreferenz 3", - "SEPA-Mandatsreferenz 4", - "SEPA-Mandatsreferenz 5", - "SEPA-Mandatsreferenz 6", - "SEPA-Mandatsreferenz 7", - "SEPA-Mandatsreferenz 8", - "SEPA-Mandatsreferenz 9", - "SEPA-Mandatsreferenz 10", - "Verknüpftes OPOS-Konto", - "Mahnsperre bis", - "Lastschriftsperre bis", - "Zahlungssperre bis", - "Gebührenberechnung", - "Mahngebühr 1", - "Mahngebühr 2", - "Mahngebühr 3", - "Pauschalberechnung", - "Verzugspauschale 1", - "Verzugspauschale 2", - "Verzugspauschale 3", - "Alternativer Suchname", - "Status", - "Anschrift manuell geändert (Korrespondenzadresse)", - "Anschrift individuell (Korrespondenzadresse)", - "Anschrift manuell geändert (Rechnungsadresse)", - "Anschrift individuell (Rechnungsadresse)", - "Fristberechnung bei Debitor", - "Mahnfrist 1", - "Mahnfrist 2", - "Mahnfrist 3", - "Letzte Frist", -] - -ACCOUNT_NAME_COLUMNS = [ - # Account number - "Konto", - # Account name - "Kontenbeschriftung", - # Language of the account name - # "de-DE" or "en-GB" - "Sprach-ID", -] - - -class DataCategory: - - """Field of the CSV Header.""" - - DEBTORS_CREDITORS = "16" - ACCOUNT_NAMES = "20" - TRANSACTIONS = "21" - POSTING_TEXT_CONSTANTS = "67" - - -class FormatName: - - """Field of the CSV Header, corresponds to DataCategory.""" - - DEBTORS_CREDITORS = "Debitoren/Kreditoren" - ACCOUNT_NAMES = "Kontenbeschriftungen" - TRANSACTIONS = "Buchungsstapel" - POSTING_TEXT_CONSTANTS = "Buchungstextkonstanten" - - -class Transactions: - DATA_CATEGORY = DataCategory.TRANSACTIONS - FORMAT_NAME = FormatName.TRANSACTIONS - FORMAT_VERSION = "9" - COLUMNS = TRANSACTION_COLUMNS - - -class DebtorsCreditors: - DATA_CATEGORY = DataCategory.DEBTORS_CREDITORS - FORMAT_NAME = FormatName.DEBTORS_CREDITORS - FORMAT_VERSION = "5" - COLUMNS = DEBTOR_CREDITOR_COLUMNS - - -class AccountNames: - DATA_CATEGORY = DataCategory.ACCOUNT_NAMES - FORMAT_NAME = FormatName.ACCOUNT_NAMES - FORMAT_VERSION = "2" - COLUMNS = ACCOUNT_NAME_COLUMNS diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py deleted file mode 100644 index d4e9c279b8..0000000000 --- a/erpnext/regional/germany/utils/datev/datev_csv.py +++ /dev/null @@ -1,184 +0,0 @@ -import datetime -import zipfile -from csv import QUOTE_NONNUMERIC -from io import BytesIO - -import frappe -import pandas as pd -from frappe import _ - -from .datev_constants import DataCategory - - -def get_datev_csv(data, filters, csv_class): - """ - Fill in missing columns and return a CSV in DATEV Format. - - For automatic processing, DATEV requires the first line of the CSV file to - hold meta data such as the length of account numbers oder the category of - the data. - - Arguments: - data -- array of dictionaries - filters -- dict - csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS - """ - empty_df = pd.DataFrame(columns=csv_class.COLUMNS) - data_df = pd.DataFrame.from_records(data) - result = empty_df.append(data_df, sort=True) - - if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS: - result["Belegdatum"] = pd.to_datetime(result["Belegdatum"]) - - result["Beleginfo - Inhalt 6"] = pd.to_datetime(result["Beleginfo - Inhalt 6"]) - result["Beleginfo - Inhalt 6"] = result["Beleginfo - Inhalt 6"].dt.strftime("%d%m%Y") - - result["Fälligkeit"] = pd.to_datetime(result["Fälligkeit"]) - result["Fälligkeit"] = result["Fälligkeit"].dt.strftime("%d%m%y") - - result.sort_values(by="Belegdatum", inplace=True, kind="stable", ignore_index=True) - - if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: - result["Sprach-ID"] = "de-DE" - - data = result.to_csv( - # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035 - sep=";", - # European decimal seperator - decimal=",", - # Windows "ANSI" encoding - encoding="latin_1", - # format date as DDMM - date_format="%d%m", - # Windows line terminator - line_terminator="\r\n", - # Do not number rows - index=False, - # Use all columns defined above - columns=csv_class.COLUMNS, - # Quote most fields, even currency values with "," separator - quoting=QUOTE_NONNUMERIC, - ) - - data = data.encode("latin_1", errors="replace") - - header = get_header(filters, csv_class) - header = ";".join(header).encode("latin_1", errors="replace") - - # 1st Row: Header with meta data - # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here. - # 3rd - nth Row: Data (Nutzdaten) - return header + b"\r\n" + data - - -def get_header(filters, csv_class): - description = filters.get("voucher_type", csv_class.FORMAT_NAME) - company = filters.get("company") - datev_settings = frappe.get_doc("DATEV Settings", {"client": company}) - default_currency = frappe.get_value("Company", company, "default_currency") - coa = frappe.get_value("Company", company, "chart_of_accounts") - coa_short_code = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "") - - header = [ - # DATEV format - # "DTVF" = created by DATEV software, - # "EXTF" = created by other software - '"EXTF"', - # version of the DATEV format - # 141 = 1.41, - # 510 = 5.10, - # 720 = 7.20 - "700", - csv_class.DATA_CATEGORY, - '"%s"' % csv_class.FORMAT_NAME, - # Format version (regarding format name) - csv_class.FORMAT_VERSION, - # Generated on - datetime.datetime.now().strftime("%Y%m%d%H%M%S") + "000", - # Imported on -- stays empty - "", - # Origin. Any two symbols, will be replaced by "SV" on import. - '"EN"', - # I = Exported by - '"%s"' % frappe.session.user, - # J = Imported by -- stays empty - "", - # K = Tax consultant number (Beraternummer) - datev_settings.get("consultant_number", "0000000"), - # L = Tax client number (Mandantennummer) - datev_settings.get("client_number", "00000"), - # M = Start of the fiscal year (Wirtschaftsjahresbeginn) - frappe.utils.formatdate(filters.get("fiscal_year_start"), "yyyyMMdd"), - # N = Length of account numbers (Sachkontenlänge) - str(filters.get("account_number_length", 4)), - # O = Transaction batch start date (YYYYMMDD) - frappe.utils.formatdate(filters.get("from_date"), "yyyyMMdd") - if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS - else "", - # P = Transaction batch end date (YYYYMMDD) - frappe.utils.formatdate(filters.get("to_date"), "yyyyMMdd") - if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS - else "", - # Q = Description (for example, "Sales Invoice") Max. 30 chars - '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", - # R = Diktatkürzel - "", - # S = Buchungstyp - # 1 = Transaction batch (Finanzbuchführung), - # 2 = Annual financial statement (Jahresabschluss) - "1" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", - # T = Rechnungslegungszweck - # 0 oder leer = vom Rechnungslegungszweck unabhängig - # 50 = Handelsrecht - # 30 = Steuerrecht - # 64 = IFRS - # 40 = Kalkulatorik - # 11 = Reserviert - # 12 = Reserviert - "0" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", - # U = Festschreibung - # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1" - "0", - # V = Default currency, for example, "EUR" - '"%s"' % default_currency if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", - # reserviert - "", - # Derivatskennzeichen - "", - # reserviert - "", - # reserviert - "", - # SKR - '"%s"' % coa_short_code, - # Branchen-Lösungs-ID - "", - # reserviert - "", - # reserviert - "", - # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung) - "", - ] - return header - - -def zip_and_download(zip_filename, csv_files): - """ - Put CSV files in a zip archive and send that to the client. - - Params: - zip_filename Name of the zip file - csv_files list of dicts [{'file_name': 'my_file.csv', 'csv_data': 'comma,separated,values'}] - """ - zip_buffer = BytesIO() - - zip_file = zipfile.ZipFile(zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED) - for csv_file in csv_files: - zip_file.writestr(csv_file.get("file_name"), csv_file.get("csv_data")) - - zip_file.close() - - frappe.response["filecontent"] = zip_buffer.getvalue() - frappe.response["filename"] = zip_filename - frappe.response["type"] = "binary" diff --git a/erpnext/regional/report/datev/__init__.py b/erpnext/regional/report/datev/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js deleted file mode 100644 index 03c729e6df..0000000000 --- a/erpnext/regional/report/datev/datev.js +++ /dev/null @@ -1,56 +0,0 @@ -frappe.query_reports["DATEV"] = { - "filters": [ - { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company") || frappe.defaults.get_global_default("Company"), - "reqd": 1 - }, - { - "fieldname": "from_date", - "label": __("From Date"), - "default": moment().subtract(1, 'month').startOf('month').format(), - "fieldtype": "Date", - "reqd": 1 - }, - { - "fieldname": "to_date", - "label": __("To Date"), - "default": moment().subtract(1, 'month').endOf('month').format(), - "fieldtype": "Date", - "reqd": 1 - }, - { - "fieldname": "voucher_type", - "label": __("Voucher Type"), - "fieldtype": "Select", - "options": "\nSales Invoice\nPurchase Invoice\nPayment Entry\nExpense Claim\nPayroll Entry\nBank Reconciliation\nAsset\nStock Entry" - } - ], - onload: function(query_report) { - let company = frappe.query_report.get_filter_value('company'); - frappe.db.exists('DATEV Settings', company).then((settings_exist) => { - if (!settings_exist) { - frappe.confirm(__('DATEV Settings for your Company are missing. Would you like to create them now?'), - () => frappe.new_doc('DATEV Settings', {'company': company}) - ); - } - }); - - query_report.page.add_menu_item(__("Download DATEV File"), () => { - const filters = encodeURIComponent( - JSON.stringify( - query_report.get_values() - ) - ); - window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`); - }); - - query_report.page.add_menu_item(__("Change DATEV Settings"), () => { - let company = frappe.query_report.get_filter_value('company'); // read company from filters again – it might have changed by now. - frappe.set_route('Form', 'DATEV Settings', company); - }); - } -}; diff --git a/erpnext/regional/report/datev/datev.json b/erpnext/regional/report/datev/datev.json deleted file mode 100644 index 94e3960ead..0000000000 --- a/erpnext/regional/report/datev/datev.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "add_total_row": 0, - "columns": [], - "creation": "2019-04-24 08:45:16.650129", - "disable_prepared_report": 0, - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "filters": [], - "idx": 0, - "is_standard": "Yes", - "modified": "2021-04-06 12:23:00.379517", - "modified_by": "Administrator", - "module": "Regional", - "name": "DATEV", - "owner": "Administrator", - "prepared_report": 0, - "ref_doctype": "GL Entry", - "report_name": "DATEV", - "report_type": "Script Report", - "roles": [] -} \ No newline at end of file diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py deleted file mode 100644 index 2d888a8762..0000000000 --- a/erpnext/regional/report/datev/datev.py +++ /dev/null @@ -1,570 +0,0 @@ -""" -Provide a report and downloadable CSV according to the German DATEV format. - -- Query report showing only the columns that contain data, formatted nicely for - dispay to the user. -- CSV download functionality `download_datev_csv` that provides a CSV file with - all required columns. Used to import the data into the DATEV Software. -""" - -import json - -import frappe -from frappe import _ - -from erpnext.accounts.utils import get_fiscal_year -from erpnext.regional.germany.utils.datev.datev_constants import ( - AccountNames, - DebtorsCreditors, - Transactions, -) -from erpnext.regional.germany.utils.datev.datev_csv import get_datev_csv, zip_and_download - -COLUMNS = [ - { - "label": "Umsatz (ohne Soll/Haben-Kz)", - "fieldname": "Umsatz (ohne Soll/Haben-Kz)", - "fieldtype": "Currency", - "width": 100, - }, - { - "label": "Soll/Haben-Kennzeichen", - "fieldname": "Soll/Haben-Kennzeichen", - "fieldtype": "Data", - "width": 100, - }, - {"label": "Konto", "fieldname": "Konto", "fieldtype": "Data", "width": 100}, - { - "label": "Gegenkonto (ohne BU-Schlüssel)", - "fieldname": "Gegenkonto (ohne BU-Schlüssel)", - "fieldtype": "Data", - "width": 100, - }, - {"label": "BU-Schlüssel", "fieldname": "BU-Schlüssel", "fieldtype": "Data", "width": 100}, - {"label": "Belegdatum", "fieldname": "Belegdatum", "fieldtype": "Date", "width": 100}, - {"label": "Belegfeld 1", "fieldname": "Belegfeld 1", "fieldtype": "Data", "width": 150}, - {"label": "Buchungstext", "fieldname": "Buchungstext", "fieldtype": "Text", "width": 300}, - { - "label": "Beleginfo - Art 1", - "fieldname": "Beleginfo - Art 1", - "fieldtype": "Link", - "options": "DocType", - "width": 100, - }, - { - "label": "Beleginfo - Inhalt 1", - "fieldname": "Beleginfo - Inhalt 1", - "fieldtype": "Dynamic Link", - "options": "Beleginfo - Art 1", - "width": 150, - }, - { - "label": "Beleginfo - Art 2", - "fieldname": "Beleginfo - Art 2", - "fieldtype": "Link", - "options": "DocType", - "width": 100, - }, - { - "label": "Beleginfo - Inhalt 2", - "fieldname": "Beleginfo - Inhalt 2", - "fieldtype": "Dynamic Link", - "options": "Beleginfo - Art 2", - "width": 150, - }, - { - "label": "Beleginfo - Art 3", - "fieldname": "Beleginfo - Art 3", - "fieldtype": "Link", - "options": "DocType", - "width": 100, - }, - { - "label": "Beleginfo - Inhalt 3", - "fieldname": "Beleginfo - Inhalt 3", - "fieldtype": "Dynamic Link", - "options": "Beleginfo - Art 3", - "width": 150, - }, - { - "label": "Beleginfo - Art 4", - "fieldname": "Beleginfo - Art 4", - "fieldtype": "Data", - "width": 100, - }, - { - "label": "Beleginfo - Inhalt 4", - "fieldname": "Beleginfo - Inhalt 4", - "fieldtype": "Data", - "width": 150, - }, - { - "label": "Beleginfo - Art 5", - "fieldname": "Beleginfo - Art 5", - "fieldtype": "Data", - "width": 150, - }, - { - "label": "Beleginfo - Inhalt 5", - "fieldname": "Beleginfo - Inhalt 5", - "fieldtype": "Data", - "width": 100, - }, - { - "label": "Beleginfo - Art 6", - "fieldname": "Beleginfo - Art 6", - "fieldtype": "Data", - "width": 150, - }, - { - "label": "Beleginfo - Inhalt 6", - "fieldname": "Beleginfo - Inhalt 6", - "fieldtype": "Date", - "width": 100, - }, - {"label": "Fälligkeit", "fieldname": "Fälligkeit", "fieldtype": "Date", "width": 100}, -] - - -def execute(filters=None): - """Entry point for frappe.""" - data = [] - if filters and validate(filters): - fn = "temporary_against_account_number" - filters[fn] = frappe.get_value("DATEV Settings", filters.get("company"), fn) - data = get_transactions(filters, as_dict=0) - - return COLUMNS, data - - -def validate(filters): - """Make sure all mandatory filters and settings are present.""" - company = filters.get("company") - if not company: - frappe.throw(_("Company is a mandatory filter.")) - - from_date = filters.get("from_date") - if not from_date: - frappe.throw(_("From Date is a mandatory filter.")) - - to_date = filters.get("to_date") - if not to_date: - frappe.throw(_("To Date is a mandatory filter.")) - - validate_fiscal_year(from_date, to_date, company) - - if not frappe.db.exists("DATEV Settings", filters.get("company")): - msg = "Please create DATEV Settings for Company {}".format(filters.get("company")) - frappe.log_error(msg, title="DATEV Settings missing") - return False - - return True - - -def validate_fiscal_year(from_date, to_date, company): - from_fiscal_year = get_fiscal_year(date=from_date, company=company) - to_fiscal_year = get_fiscal_year(date=to_date, company=company) - if from_fiscal_year != to_fiscal_year: - frappe.throw(_("Dates {} and {} are not in the same fiscal year.").format(from_date, to_date)) - - -def get_transactions(filters, as_dict=1): - def run(params_method, filters): - extra_fields, extra_joins, extra_filters = params_method(filters) - return run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=as_dict) - - def sort_by(row): - # "Belegdatum" is in the fifth column when list format is used - return row["Belegdatum" if as_dict else 5] - - type_map = { - # specific query methods for some voucher types - "Payment Entry": get_payment_entry_params, - "Sales Invoice": get_sales_invoice_params, - "Purchase Invoice": get_purchase_invoice_params, - } - - only_voucher_type = filters.get("voucher_type") - transactions = [] - - for voucher_type, get_voucher_params in type_map.items(): - if only_voucher_type and only_voucher_type != voucher_type: - continue - - transactions.extend(run(params_method=get_voucher_params, filters=filters)) - - if not only_voucher_type or only_voucher_type not in type_map: - # generic query method for all other voucher types - filters["exclude_voucher_types"] = type_map.keys() - transactions.extend(run(params_method=get_generic_params, filters=filters)) - - return sorted(transactions, key=sort_by) - - -def get_payment_entry_params(filters): - extra_fields = """ - , 'Zahlungsreferenz' as 'Beleginfo - Art 5' - , pe.reference_no as 'Beleginfo - Inhalt 5' - , 'Buchungstag' as 'Beleginfo - Art 6' - , pe.reference_date as 'Beleginfo - Inhalt 6' - , '' as 'Fälligkeit' - """ - - extra_joins = """ - LEFT JOIN `tabPayment Entry` pe - ON gl.voucher_no = pe.name - """ - - extra_filters = """ - AND gl.voucher_type = 'Payment Entry' - """ - - return extra_fields, extra_joins, extra_filters - - -def get_sales_invoice_params(filters): - extra_fields = """ - , '' as 'Beleginfo - Art 5' - , '' as 'Beleginfo - Inhalt 5' - , '' as 'Beleginfo - Art 6' - , '' as 'Beleginfo - Inhalt 6' - , si.due_date as 'Fälligkeit' - """ - - extra_joins = """ - LEFT JOIN `tabSales Invoice` si - ON gl.voucher_no = si.name - """ - - extra_filters = """ - AND gl.voucher_type = 'Sales Invoice' - """ - - return extra_fields, extra_joins, extra_filters - - -def get_purchase_invoice_params(filters): - extra_fields = """ - , 'Lieferanten-Rechnungsnummer' as 'Beleginfo - Art 5' - , pi.bill_no as 'Beleginfo - Inhalt 5' - , 'Lieferanten-Rechnungsdatum' as 'Beleginfo - Art 6' - , pi.bill_date as 'Beleginfo - Inhalt 6' - , pi.due_date as 'Fälligkeit' - """ - - extra_joins = """ - LEFT JOIN `tabPurchase Invoice` pi - ON gl.voucher_no = pi.name - """ - - extra_filters = """ - AND gl.voucher_type = 'Purchase Invoice' - """ - - return extra_fields, extra_joins, extra_filters - - -def get_generic_params(filters): - # produce empty fields so all rows will have the same length - extra_fields = """ - , '' as 'Beleginfo - Art 5' - , '' as 'Beleginfo - Inhalt 5' - , '' as 'Beleginfo - Art 6' - , '' as 'Beleginfo - Inhalt 6' - , '' as 'Fälligkeit' - """ - extra_joins = "" - - if filters.get("exclude_voucher_types"): - # exclude voucher types that are queried by a dedicated method - exclude = "({})".format( - ", ".join("'{}'".format(key) for key in filters.get("exclude_voucher_types")) - ) - extra_filters = "AND gl.voucher_type NOT IN {}".format(exclude) - - # if voucher type filter is set, allow only this type - if filters.get("voucher_type"): - extra_filters += " AND gl.voucher_type = %(voucher_type)s" - - return extra_fields, extra_joins, extra_filters - - -def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1): - """ - Get a list of accounting entries. - - Select GL Entries joined with Account and Party Account in order to get the - account numbers. Returns a list of accounting entries. - - Arguments: - filters -- dict of filters to be passed to the sql query - as_dict -- return as list of dicts [0,1] - """ - query = """ - SELECT - - /* either debit or credit amount; always positive */ - case gl.debit when 0 then gl.credit else gl.debit end as 'Umsatz (ohne Soll/Haben-Kz)', - - /* 'H' when credit, 'S' when debit */ - case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', - - /* account number or, if empty, party account number */ - acc.account_number as 'Konto', - - /* against number or, if empty, party against number */ - %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', - - '' as 'BU-Schlüssel', - - gl.posting_date as 'Belegdatum', - gl.voucher_no as 'Belegfeld 1', - REPLACE(LEFT(gl.remarks, 60), '\n', ' ') as 'Buchungstext', - gl.voucher_type as 'Beleginfo - Art 1', - gl.voucher_no as 'Beleginfo - Inhalt 1', - gl.against_voucher_type as 'Beleginfo - Art 2', - gl.against_voucher as 'Beleginfo - Inhalt 2', - gl.party_type as 'Beleginfo - Art 3', - gl.party as 'Beleginfo - Inhalt 3', - case gl.party_type when 'Customer' then 'Debitorennummer' when 'Supplier' then 'Kreditorennummer' else NULL end as 'Beleginfo - Art 4', - par.debtor_creditor_number as 'Beleginfo - Inhalt 4' - - {extra_fields} - - FROM `tabGL Entry` gl - - /* Kontonummer */ - LEFT JOIN `tabAccount` acc - ON gl.account = acc.name - - LEFT JOIN `tabParty Account` par - ON par.parent = gl.party - AND par.parenttype = gl.party_type - AND par.company = %(company)s - - {extra_joins} - - WHERE gl.company = %(company)s - AND DATE(gl.posting_date) >= %(from_date)s - AND DATE(gl.posting_date) <= %(to_date)s - - {extra_filters} - - ORDER BY 'Belegdatum', gl.voucher_no""".format( - extra_fields=extra_fields, extra_joins=extra_joins, extra_filters=extra_filters - ) - - gl_entries = frappe.db.sql(query, filters, as_dict=as_dict) - - return gl_entries - - -def get_customers(filters): - """ - Get a list of Customers. - - Arguments: - filters -- dict of filters to be passed to the sql query - """ - return frappe.db.sql( - """ - SELECT - - par.debtor_creditor_number as 'Konto', - CASE cus.customer_type - WHEN 'Company' THEN cus.customer_name - ELSE null - END as 'Name (Adressatentyp Unternehmen)', - CASE cus.customer_type - WHEN 'Individual' THEN TRIM(SUBSTR(cus.customer_name, LOCATE(' ', cus.customer_name))) - ELSE null - END as 'Name (Adressatentyp natürl. Person)', - CASE cus.customer_type - WHEN 'Individual' THEN SUBSTRING_INDEX(SUBSTRING_INDEX(cus.customer_name, ' ', 1), ' ', -1) - ELSE null - END as 'Vorname (Adressatentyp natürl. Person)', - CASE cus.customer_type - WHEN 'Individual' THEN '1' - WHEN 'Company' THEN '2' - ELSE '0' - END as 'Adressatentyp', - adr.address_line1 as 'Straße', - adr.pincode as 'Postleitzahl', - adr.city as 'Ort', - UPPER(country.code) as 'Land', - adr.address_line2 as 'Adresszusatz', - adr.email_id as 'E-Mail', - adr.phone as 'Telefon', - adr.fax as 'Fax', - cus.website as 'Internet', - cus.tax_id as 'Steuernummer' - - FROM `tabCustomer` cus - - left join `tabParty Account` par - on par.parent = cus.name - and par.parenttype = 'Customer' - and par.company = %(company)s - - left join `tabDynamic Link` dyn_adr - on dyn_adr.link_name = cus.name - and dyn_adr.link_doctype = 'Customer' - and dyn_adr.parenttype = 'Address' - - left join `tabAddress` adr - on adr.name = dyn_adr.parent - and adr.is_primary_address = '1' - - left join `tabCountry` country - on country.name = adr.country - - WHERE adr.is_primary_address = '1' - """, - filters, - as_dict=1, - ) - - -def get_suppliers(filters): - """ - Get a list of Suppliers. - - Arguments: - filters -- dict of filters to be passed to the sql query - """ - return frappe.db.sql( - """ - SELECT - - par.debtor_creditor_number as 'Konto', - CASE sup.supplier_type - WHEN 'Company' THEN sup.supplier_name - ELSE null - END as 'Name (Adressatentyp Unternehmen)', - CASE sup.supplier_type - WHEN 'Individual' THEN TRIM(SUBSTR(sup.supplier_name, LOCATE(' ', sup.supplier_name))) - ELSE null - END as 'Name (Adressatentyp natürl. Person)', - CASE sup.supplier_type - WHEN 'Individual' THEN SUBSTRING_INDEX(SUBSTRING_INDEX(sup.supplier_name, ' ', 1), ' ', -1) - ELSE null - END as 'Vorname (Adressatentyp natürl. Person)', - CASE sup.supplier_type - WHEN 'Individual' THEN '1' - WHEN 'Company' THEN '2' - ELSE '0' - END as 'Adressatentyp', - adr.address_line1 as 'Straße', - adr.pincode as 'Postleitzahl', - adr.city as 'Ort', - UPPER(country.code) as 'Land', - adr.address_line2 as 'Adresszusatz', - adr.email_id as 'E-Mail', - adr.phone as 'Telefon', - adr.fax as 'Fax', - sup.website as 'Internet', - sup.tax_id as 'Steuernummer', - case sup.on_hold when 1 then sup.release_date else null end as 'Zahlungssperre bis' - - FROM `tabSupplier` sup - - left join `tabParty Account` par - on par.parent = sup.name - and par.parenttype = 'Supplier' - and par.company = %(company)s - - left join `tabDynamic Link` dyn_adr - on dyn_adr.link_name = sup.name - and dyn_adr.link_doctype = 'Supplier' - and dyn_adr.parenttype = 'Address' - - left join `tabAddress` adr - on adr.name = dyn_adr.parent - and adr.is_primary_address = '1' - - left join `tabCountry` country - on country.name = adr.country - - WHERE adr.is_primary_address = '1' - """, - filters, - as_dict=1, - ) - - -def get_account_names(filters): - return frappe.db.sql( - """ - SELECT - - account_number as 'Konto', - LEFT(account_name, 40) as 'Kontenbeschriftung', - 'de-DE' as 'Sprach-ID' - - FROM `tabAccount` - WHERE company = %(company)s - AND is_group = 0 - AND account_number != '' - """, - filters, - as_dict=1, - ) - - -@frappe.whitelist() -def download_datev_csv(filters): - """ - Provide accounting entries for download in DATEV format. - - Validate the filters, get the data, produce the CSV file and provide it for - download. Can be called like this: - - GET /api/method/erpnext.regional.report.datev.datev.download_datev_csv - - Arguments / Params: - filters -- dict of filters to be passed to the sql query - """ - if isinstance(filters, str): - filters = json.loads(filters) - - validate(filters) - company = filters.get("company") - - fiscal_year = get_fiscal_year(date=filters.get("from_date"), company=company) - filters["fiscal_year_start"] = fiscal_year[1] - - # set chart of accounts used - coa = frappe.get_value("Company", company, "chart_of_accounts") - filters["skr"] = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "") - - datev_settings = frappe.get_doc("DATEV Settings", company) - filters["account_number_length"] = datev_settings.account_number_length - filters["temporary_against_account_number"] = datev_settings.temporary_against_account_number - - transactions = get_transactions(filters) - account_names = get_account_names(filters) - customers = get_customers(filters) - suppliers = get_suppliers(filters) - - zip_name = "{} DATEV.zip".format(frappe.utils.datetime.date.today()) - zip_and_download( - zip_name, - [ - { - "file_name": "EXTF_Buchungsstapel.csv", - "csv_data": get_datev_csv(transactions, filters, csv_class=Transactions), - }, - { - "file_name": "EXTF_Kontenbeschriftungen.csv", - "csv_data": get_datev_csv(account_names, filters, csv_class=AccountNames), - }, - { - "file_name": "EXTF_Kunden.csv", - "csv_data": get_datev_csv(customers, filters, csv_class=DebtorsCreditors), - }, - { - "file_name": "EXTF_Lieferanten.csv", - "csv_data": get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors), - }, - ], - ) diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py deleted file mode 100644 index 0df8c0607d..0000000000 --- a/erpnext/regional/report/datev/test_datev.py +++ /dev/null @@ -1,252 +0,0 @@ -import zipfile -from io import BytesIO -from unittest import TestCase - -import frappe -from frappe.utils import cstr, now_datetime, today - -from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.regional.germany.utils.datev.datev_constants import ( - AccountNames, - DebtorsCreditors, - Transactions, -) -from erpnext.regional.germany.utils.datev.datev_csv import get_datev_csv, get_header -from erpnext.regional.report.datev.datev import ( - download_datev_csv, - get_account_names, - get_customers, - get_suppliers, - get_transactions, -) - - -def make_company(company_name, abbr): - if not frappe.db.exists("Company", company_name): - company = frappe.get_doc( - { - "doctype": "Company", - "company_name": company_name, - "abbr": abbr, - "default_currency": "EUR", - "country": "Germany", - "create_chart_of_accounts_based_on": "Standard Template", - "chart_of_accounts": "SKR04 mit Kontonummern", - } - ) - company.insert() - else: - company = frappe.get_doc("Company", company_name) - - # indempotent - company.create_default_warehouses() - - if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}): - company.create_default_cost_center() - - company.save() - return company - - -def setup_fiscal_year(): - fiscal_year = None - year = cstr(now_datetime().year) - if not frappe.db.get_value("Fiscal Year", {"year": year}, "name"): - try: - fiscal_year = frappe.get_doc( - { - "doctype": "Fiscal Year", - "year": year, - "year_start_date": "{0}-01-01".format(year), - "year_end_date": "{0}-12-31".format(year), - } - ) - fiscal_year.insert() - except frappe.NameError: - pass - - if fiscal_year: - fiscal_year.set_as_default() - - -def make_customer_with_account(customer_name, company): - acc_name = frappe.db.get_value( - "Account", {"account_name": customer_name, "company": company.name}, "name" - ) - - if not acc_name: - acc = frappe.get_doc( - { - "doctype": "Account", - "parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG", - "account_name": customer_name, - "company": company.name, - "account_type": "Receivable", - "account_number": "10001", - } - ) - acc.insert() - acc_name = acc.name - - if not frappe.db.exists("Customer", customer_name): - customer = frappe.get_doc( - { - "doctype": "Customer", - "customer_name": customer_name, - "customer_type": "Company", - "accounts": [{"company": company.name, "account": acc_name}], - } - ) - customer.insert() - else: - customer = frappe.get_doc("Customer", customer_name) - - return customer - - -def make_item(item_code, company): - warehouse_name = frappe.db.get_value( - "Warehouse", {"warehouse_name": "Stores", "company": company.name}, "name" - ) - - if not frappe.db.exists("Item", item_code): - item = frappe.get_doc( - { - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "All Item Groups", - "is_stock_item": 0, - "is_purchase_item": 0, - "is_customer_provided_item": 0, - "item_defaults": [{"default_warehouse": warehouse_name, "company": company.name}], - } - ) - item.insert() - else: - item = frappe.get_doc("Item", item_code) - return item - - -def make_datev_settings(company): - if not frappe.db.exists("DATEV Settings", company.name): - frappe.get_doc( - { - "doctype": "DATEV Settings", - "client": company.name, - "client_number": "12345", - "consultant_number": "67890", - "temporary_against_account_number": "9999", - } - ).insert() - - -class TestDatev(TestCase): - def setUp(self): - self.company = make_company("_Test GmbH", "_TG") - self.customer = make_customer_with_account("_Test Kunde GmbH", self.company) - self.filters = { - "company": self.company.name, - "from_date": today(), - "to_date": today(), - "temporary_against_account_number": "9999", - } - - make_datev_settings(self.company) - item = make_item("_Test Item", self.company) - setup_fiscal_year() - - warehouse = frappe.db.get_value( - "Item Default", {"parent": item.name, "company": self.company.name}, "default_warehouse" - ) - - income_account = frappe.db.get_value( - "Account", {"account_number": "4200", "company": self.company.name}, "name" - ) - - tax_account = frappe.db.get_value( - "Account", {"account_number": "3806", "company": self.company.name}, "name" - ) - - si = create_sales_invoice( - company=self.company.name, - customer=self.customer.name, - currency=self.company.default_currency, - debit_to=self.customer.accounts[0].account, - income_account="4200 - Erlöse - _TG", - expense_account="6990 - Herstellungskosten - _TG", - cost_center=self.company.cost_center, - warehouse=warehouse, - item=item.name, - do_not_save=1, - ) - - si.append( - "taxes", - { - "charge_type": "On Net Total", - "account_head": tax_account, - "description": "Umsatzsteuer 19 %", - "rate": 19, - "cost_center": self.company.cost_center, - }, - ) - - si.cost_center = self.company.cost_center - - si.save() - si.submit() - - def test_columns(self): - def is_subset(get_data, allowed_keys): - """ - Validate that the dict contains only allowed keys. - - Params: - get_data -- Function that returns a list of dicts. - allowed_keys -- List of allowed keys - """ - data = get_data(self.filters) - if data == []: - # No data and, therefore, no columns is okay - return True - actual_set = set(data[0].keys()) - # allowed set must be interpreted as unicode to match the actual set - allowed_set = set({frappe.as_unicode(key) for key in allowed_keys}) - return actual_set.issubset(allowed_set) - - self.assertTrue(is_subset(get_transactions, Transactions.COLUMNS)) - self.assertTrue(is_subset(get_customers, DebtorsCreditors.COLUMNS)) - self.assertTrue(is_subset(get_suppliers, DebtorsCreditors.COLUMNS)) - self.assertTrue(is_subset(get_account_names, AccountNames.COLUMNS)) - - def test_header(self): - self.assertTrue(Transactions.DATA_CATEGORY in get_header(self.filters, Transactions)) - self.assertTrue(AccountNames.DATA_CATEGORY in get_header(self.filters, AccountNames)) - self.assertTrue(DebtorsCreditors.DATA_CATEGORY in get_header(self.filters, DebtorsCreditors)) - - def test_csv(self): - test_data = [ - { - "Umsatz (ohne Soll/Haben-Kz)": 100, - "Soll/Haben-Kennzeichen": "H", - "Kontonummer": "4200", - "Gegenkonto (ohne BU-Schlüssel)": "10000", - "Belegdatum": today(), - "Buchungstext": "No remark", - "Beleginfo - Art 1": "Sales Invoice", - "Beleginfo - Inhalt 1": "SINV-0001", - } - ] - get_datev_csv(data=test_data, filters=self.filters, csv_class=Transactions) - - def test_download(self): - """Assert that the returned file is a ZIP file.""" - download_datev_csv(self.filters) - - # zipfile.is_zipfile() expects a file-like object - zip_buffer = BytesIO() - zip_buffer.write(frappe.response["filecontent"]) - - self.assertTrue(zipfile.is_zipfile(zip_buffer)) From 0f09042962ccccf9f00cf9427b9c9f31d0ceb706 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 13 Apr 2022 15:25:47 +0530 Subject: [PATCH 24/33] chore(deps)!: Drop `pandas` as dependency (#30598) depends on https://github.com/frappe/erpnext/pull/30597 depends on https://github.com/frappe/erpnext/pull/30584 Closes https://github.com/frappe/erpnext/issues/27047 (read for more details) --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 657054fb24..85ff515772 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ # frappe # https://github.com/frappe/frappe is installed during bench-init gocardless-pro~=1.22.0 googlemaps -pandas>=1.1.5,<2.0.0 plaid-python~=7.2.1 pycountry~=20.7.3 PyGithub~=1.55 From 71de754368f9d794c9c3b9d5a6ac6810f3a7cc09 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 13 Apr 2022 18:23:22 +0530 Subject: [PATCH 25/33] ci: coverage not getting processed (#30704) --- .github/workflows/server-tests-mariadb.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index 8cc582634a..cdb68499ff 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -129,6 +129,9 @@ jobs: needs: test runs-on: ubuntu-latest steps: + - name: Clone + uses: actions/checkout@v2 + - name: Download artifacts uses: actions/download-artifact@v3 From cef28df8db6ab14e4d243c1ea302bb5f4e57b528 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 13 Apr 2022 19:10:19 +0530 Subject: [PATCH 26/33] 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 dcdba095fb..064b806e95 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 a26ce2383d..04e8211214 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 67bc5bdbb8..bb916a8092 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 ff41b8da4e3ae40b07847193c0ad40848feb9ced Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 13 Apr 2022 20:12:08 +0530 Subject: [PATCH 27/33] fix: Update received_by if "to" is changed --- erpnext/telephony/doctype/call_log/call_log.py | 16 ++++++++++------ erpnext/tests/test_exotel.py | 1 + 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 2092ec284b..7725e71f19 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): @@ -114,7 +118,7 @@ def get_employees_with_number(number): employee_doc_name_and_emails = frappe.get_all( "Employee", - filters={"cell_number": ["like", "%{}%".format(number)], "user_id": ["!=", ""]}, + filters={"cell_number": ["like", f"%{number}%"], "user_id": ["!=", ""]}, fields=["name", "user_id"], ) diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index bb916a8092..76bbb3e05a 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 79fbc2c504f94d898e118a9af0f37725e79c4323 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 13 Apr 2022 20:42:25 +0530 Subject: [PATCH 28/33] test: flaky PR close test (#30709) --- .../doctype/quality_procedure/test_quality_procedure.py | 1 + .../doctype/purchase_receipt/test_purchase_receipt.py | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) 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 04e8211214..2a47a01a58 100644 --- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py @@ -19,6 +19,7 @@ class TestQualityProcedure(unittest.TestCase): ) ).insert() + frappe.local.form_dict = frappe._dict( doctype="Quality Procedure", quality_procedure_name="Test Child 1", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index f3faba4f8d..ce3bd56d55 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -747,14 +747,13 @@ class TestPurchaseReceipt(FrappeTestCase): update_purchase_receipt_status, ) - pr = make_purchase_receipt() + item = make_item() + + pr = make_purchase_receipt(item_code=item.name) update_purchase_receipt_status(pr.name, "Closed") self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed") - pr.reload() - pr.cancel() - def test_pr_billing_status(self): """Flow: 1. PO -> PR1 -> PI From c272170b86fb764992b86c46a93ede9ce96d272e Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 14 Apr 2022 09:51:54 +0530 Subject: [PATCH 29/33] fix(call log): Convert `Data` to `Link` for type_of_call Convert `Data` to `Link` for type_of_call, employee_user_id and call_received_by fields --- erpnext/telephony/doctype/call_log/call_log.json | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index cd749e8a01..c0d416c1c2 100644 --- a/erpnext/telephony/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -141,27 +141,30 @@ }, { "fieldname": "employee_user_id", - "fieldtype": "Data", + "fieldtype": "Link", "hidden": 1, - "label": "Employee User Id" + "label": "Employee User Id", + "options": "Employee" }, { "fieldname": "type_of_call", - "fieldtype": "Data", - "label": "Type Of Call" + "fieldtype": "Link", + "label": "Type Of Call", + "options": "Telephony Call Type" }, { "depends_on": "to", "fieldname": "call_received_by", - "fieldtype": "Data", + "fieldtype": "Link", "label": "Call Received By", + "options": "User", "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-02-25 14:37:48.575230", + "modified": "2022-04-14 00:18:31.148428", "modified_by": "Administrator", "module": "Telephony", "name": "Call Log", From 6e837bcdfc224df4525a256305636eb57348fa16 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 14 Apr 2022 10:02:15 +0530 Subject: [PATCH 30/33] style: Fix linter warning --- .../doctype/quality_procedure/test_quality_procedure.py | 1 - 1 file changed, 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 2a47a01a58..04e8211214 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,6 @@ class TestQualityProcedure(unittest.TestCase): ) ).insert() - frappe.local.form_dict = frappe._dict( doctype="Quality Procedure", quality_procedure_name="Test Child 1", From e6cb948a963bae6d4ccfe04274f93c21cfee664a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 13 Apr 2022 12:21:33 +0530 Subject: [PATCH 31/33] fix: Payment reco query with max invocie and payment amount limit (cherry picked from commit 4008c95ac6afc3b06a0a602e3f469db61e1f0453) --- .../payment_reconciliation.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 907b76915a..b596df9224 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -350,9 +350,13 @@ class PaymentReconciliation(Document): ) if self.minimum_invoice_amount: - condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount)) + condition += " and {dr_or_cr} >= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount) + ) if self.maximum_invoice_amount: - condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount)) + condition += " and {dr_or_cr} <= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount) + ) elif get_return_invoices: condition = " and doc.company = '{0}' ".format(self.company) @@ -367,15 +371,19 @@ class PaymentReconciliation(Document): else "" ) dr_or_cr = ( - "gl.debit_in_account_currency" + "debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == "Receivable" - else "gl.credit_in_account_currency" + else "credit_in_account_currency" ) if self.minimum_invoice_amount: - condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount)) + condition += " and gl.{dr_or_cr} >= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount) + ) if self.maximum_invoice_amount: - condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount)) + condition += " and gl.{dr_or_cr} <= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount) + ) else: condition += ( From ad171c6225fc50305e565ce2a67bc60fc1e85722 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 14 Apr 2022 12:03:12 +0530 Subject: [PATCH 32/33] test: Update test case --- .../doctype/purchase_invoice/test_purchase_invoice.py | 5 ++++- erpnext/stock/doctype/item/item.js | 4 ++-- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 73390dd6f4..59bd637e41 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1482,7 +1482,8 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) def test_provisional_accounting_entry(self): - item = create_item("_Test Non Stock Item", is_stock_item=0) + create_item("_Test Non Stock Item", is_stock_item=0) + provisional_account = create_account( account_name="Provision Account", parent_account="Current Liabilities - _TC", @@ -1505,6 +1506,8 @@ class TestPurchaseInvoice(unittest.TestCase): pi.save() pi.submit() + self.assertEquals(pr.items[0].provisional_expense_account, "Provision Account - _TC") + # Check GLE for Purchase Invoice expected_gle = [ ["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)], diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 3abb609302..ae8b488479 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -385,8 +385,8 @@ $.extend(erpnext.item, { "root_type": ["in", ["Liability", "Asset"]], "is_group": 0 } - } - }) + }; + }); }, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d050624b23..ec0e809aa6 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -512,7 +512,9 @@ class PurchaseReceipt(BuyingController): and flt(d.qty) and provisional_accounting_for_non_stock_items ): - self.add_provisional_gl_entry(d, gl_entries, self.posting_date) + self.add_provisional_gl_entry( + d, gl_entries, self.posting_date, d.get("provisional_expense_account") + ) if warehouse_with_no_account: frappe.msgprint( From 77599fe44301e79b24d96c00f7a4d3ecbf6af901 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 14 Apr 2022 12:32:31 +0530 Subject: [PATCH 33/33] fix: Set correct options for link field issue introduced via https://github.com/frappe/erpnext/pull/30711 --- erpnext/telephony/doctype/call_log/call_log.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index c0d416c1c2..a41ddb19f9 100644 --- a/erpnext/telephony/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -144,7 +144,7 @@ "fieldtype": "Link", "hidden": 1, "label": "Employee User Id", - "options": "Employee" + "options": "User" }, { "fieldname": "type_of_call", @@ -157,14 +157,14 @@ "fieldname": "call_received_by", "fieldtype": "Link", "label": "Call Received By", - "options": "User", + "options": "Employee", "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-04-14 00:18:31.148428", + "modified": "2022-04-14 02:59:22.503202", "modified_by": "Administrator", "module": "Telephony", "name": "Call Log",