From 8d3011832be06f651085994b148022ff635f4e0d Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 23 Feb 2022 12:59:38 +0530 Subject: [PATCH 01/49] 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 167fcb71652..c4f6636239f 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/49] fix: added employee name to call log --- erpnext/telephony/doctype/call_log/call_log.json | 14 ++++++++++++-- erpnext/telephony/doctype/call_log/call_log.py | 9 +++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index 1d6c39edf6e..a3dbb02213f 100644 --- a/erpnext/telephony/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "field:id", - "creation": "2019-06-05 12:07:02.634534", + "creation": "2022-02-21 11:54:58.414784", "doctype": "DocType", "engine": "InnoDB", "field_order": [ @@ -9,6 +9,7 @@ "id", "from", "to", + "employee_call_directed_to", "medium", "start_time", "end_time", @@ -134,15 +135,23 @@ "fieldname": "call_details_section", "fieldtype": "Section Break", "label": "Call Details" + }, + { + "depends_on": "to", + "fieldname": "employee_call_directed_to", + "fieldtype": "Data", + "label": "Employee Call Directed To", + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-08 14:23:28.744844", + "modified": "2022-02-23 18:45:06.932571", "modified_by": "Administrator", "module": "Telephony", "name": "Call Log", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -164,6 +173,7 @@ ], "sort_field": "creation", "sort_order": "DESC", + "states": [], "title_field": "from", "track_changes": 1, "track_views": 1 diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 0c24484bdfb..7d86d12cf43 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/49] fix: added field to show called group, user_id --- erpnext/telephony/doctype/call_log/call_log.json | 9 ++++++++- erpnext/telephony/doctype/call_log/call_log.py | 12 ++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index a3dbb02213f..c0f022b0209 100644 --- a/erpnext/telephony/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -10,6 +10,7 @@ "from", "to", "employee_call_directed_to", + "employee_user_id", "medium", "start_time", "end_time", @@ -142,12 +143,18 @@ "fieldtype": "Data", "label": "Employee Call Directed To", "read_only": 1 + }, + { + "fieldname": "employee_user_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Employee User Id" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-02-23 18:45:06.932571", + "modified": "2022-02-23 19:47:04.310577", "modified_by": "Administrator", "module": "Telephony", "name": "Call Log", diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 7d86d12cf43..787015169b3 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/49] 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 c4f6636239f..a94596933d7 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 c954f12ac63..2addeae0e5e 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -141,6 +141,15 @@ class CallPopup { 'fieldtype': 'Section Break', 'hide_border': 1, }, { + 'fieldname': 'type_of_call', + 'label': 'Type Of Call', + 'fieldtype': 'Select', + 'options': '\nFrappe Cloud Queries/Plan\nEnterprise Plans\nPartnership\nSupport\nBilling/Renewal\nOpen source / Junk', + 'default': 'Frappe Cloud Queries/Plan', + },{ + 'fieldtype': 'Section Break', + 'hide_border': 1, + },{ 'fieldtype': 'Small Text', 'label': __('Call Summary'), 'fieldname': 'call_summary', @@ -149,10 +158,12 @@ class CallPopup { 'label': __('Save'), 'click': () => { const call_summary = this.call_details.get_value('call_summary'); + const call_type = this.call_details.get_value('type_of_call'); if (!call_summary) return; - frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', { + frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary_and_call_type', { 'call_log': this.call_log.name, 'summary': call_summary, + 'call_type': call_type, }).then(() => { this.close_modal(); frappe.show_alert({ diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index c0f022b0209..615e069e72f 100644 --- a/erpnext/telephony/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -22,6 +22,7 @@ "recording_url", "recording_html", "section_break_11", + "type_of_call", "summary", "section_break_19", "links" @@ -105,7 +106,8 @@ }, { "fieldname": "summary", - "fieldtype": "Small Text" + "fieldtype": "Small Text", + "label": "Summary" }, { "fieldname": "section_break_11", @@ -149,12 +151,17 @@ "fieldtype": "Data", "hidden": 1, "label": "Employee User Id" + }, + { + "fieldname": "type_of_call", + "fieldtype": "Data", + "label": "Type Of Call" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-02-23 19:47:04.310577", + "modified": "2022-02-25 13:37:48.156501", "modified_by": "Administrator", "module": "Telephony", "name": "Call Log", diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 787015169b3..e55d2906c9c 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/49] fix: sider fixes --- erpnext/public/js/call_popup/call_popup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 2addeae0e5e..b7d3485b657 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -146,10 +146,10 @@ class CallPopup { 'fieldtype': 'Select', 'options': '\nFrappe Cloud Queries/Plan\nEnterprise Plans\nPartnership\nSupport\nBilling/Renewal\nOpen source / Junk', 'default': 'Frappe Cloud Queries/Plan', - },{ + }, { 'fieldtype': 'Section Break', 'hide_border': 1, - },{ + }, { 'fieldtype': 'Small Text', 'label': __('Call Summary'), 'fieldname': 'call_summary', From 5b79496f057c79163fcea8f165914d109cdbc35f Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Fri, 25 Feb 2022 16:52:25 +0530 Subject: [PATCH 06/49] 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 a94596933d7..502dbed4f73 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 2addeae0e5e..4d69c4ba595 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -141,11 +141,10 @@ class CallPopup { 'fieldtype': 'Section Break', 'hide_border': 1, }, { - 'fieldname': 'type_of_call', - 'label': 'Type Of Call', - 'fieldtype': 'Select', - 'options': '\nFrappe Cloud Queries/Plan\nEnterprise Plans\nPartnership\nSupport\nBilling/Renewal\nOpen source / Junk', - 'default': 'Frappe Cloud Queries/Plan', + 'fieldname': 'call_type', + 'label': 'Call Type', + 'fieldtype': 'Link', + 'options': 'Telephony Call Type', },{ 'fieldtype': 'Section Break', 'hide_border': 1, @@ -158,7 +157,7 @@ class CallPopup { 'label': __('Save'), 'click': () => { const call_summary = this.call_details.get_value('call_summary'); - const call_type = this.call_details.get_value('type_of_call'); + const call_type = this.call_details.get_value('call_type'); if (!call_summary) return; frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary_and_call_type', { 'call_log': this.call_log.name, diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index 615e069e72f..cd749e8a018 100644 --- a/erpnext/telephony/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -9,7 +9,7 @@ "id", "from", "to", - "employee_call_directed_to", + "call_received_by", "employee_user_id", "medium", "start_time", @@ -139,13 +139,6 @@ "fieldtype": "Section Break", "label": "Call Details" }, - { - "depends_on": "to", - "fieldname": "employee_call_directed_to", - "fieldtype": "Data", - "label": "Employee Call Directed To", - "read_only": 1 - }, { "fieldname": "employee_user_id", "fieldtype": "Data", @@ -156,12 +149,19 @@ "fieldname": "type_of_call", "fieldtype": "Data", "label": "Type Of Call" + }, + { + "depends_on": "to", + "fieldname": "call_received_by", + "fieldtype": "Data", + "label": "Call Received By", + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-02-25 13:37:48.156501", + "modified": "2022-02-25 14:37:48.575230", "modified_by": "Administrator", "module": "Telephony", "name": "Call Log", diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index e55d2906c9c..7b81a29fc18 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 00000000000..e69de29bb2d diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js new file mode 100644 index 00000000000..efba2b86ff5 --- /dev/null +++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Telephony Call Type', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json new file mode 100644 index 00000000000..603709e98f9 --- /dev/null +++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json @@ -0,0 +1,58 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:call_type", + "creation": "2022-02-25 16:13:37.321312", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "call_type", + "amended_from" + ], + "fields": [ + { + "fieldname": "call_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Call Type", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Telephony Call Type", + "print_hide": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2022-02-25 16:14:07.087461", + "modified_by": "Administrator", + "module": "Telephony", + "name": "Telephony Call Type", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py new file mode 100644 index 00000000000..5abb3180df1 --- /dev/null +++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class TelephonyCallType(Document): + pass diff --git a/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py new file mode 100644 index 00000000000..c16d03e0978 --- /dev/null +++ b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestTelephonyCallType(unittest.TestCase): + pass From 1ab5b7a811674b808d42ab8b7b2ad924531cc247 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 28 Feb 2022 12:47:05 +0530 Subject: [PATCH 07/49] 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 119aad7e6e8..2dbe999e05b 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 5abb3180df1..944ffef36f2 100644 --- a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py +++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class TelephonyCallType(Document): pass diff --git a/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py index c16d03e0978..b3c19c39102 100644 --- a/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py +++ b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestTelephonyCallType(unittest.TestCase): pass From b19305aa8350c810535c94dc6af7ba0b9e2dde89 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 22 Mar 2022 16:22:09 +0530 Subject: [PATCH 08/49] 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 d592a9c79e2..8a12f3b1d07 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 7b81a29fc18..f3c941ed9cf 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 485e6b6a0931c9ec9709879d878894ea8760e7ca Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 27 Mar 2022 13:02:31 +0530 Subject: [PATCH 09/49] fix: Incorrect default amount to pay for POS invoices (cherry picked from commit a044e9268786b3395f2e33fd5b79e64bc60160a5) # Conflicts: # erpnext/public/js/controllers/taxes_and_totals.js --- erpnext/public/js/controllers/taxes_and_totals.js | 9 ++++++--- erpnext/public/js/controllers/transaction.js | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 9fec43b74ef..c861ebb19df 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -34,12 +34,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { frappe.model.set_value(item.doctype, item.name, "rate", item_rate); } +<<<<<<< HEAD calculate_taxes_and_totals(update_paid_amount) { +======= + calculate_taxes_and_totals: async function(update_paid_amount) { +>>>>>>> a044e92687 (fix: Incorrect default amount to pay for POS invoices) this.discount_amount_applied = false; this._calculate_taxes_and_totals(); this.calculate_discount_amount(); - this.calculate_shipping_charges(); + await this.calculate_shipping_charges(); // Advance calculation applicable to Sales /Purchase Invoice if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype) @@ -275,8 +279,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { calculate_shipping_charges() { frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) { - this.shipping_rule(); - this._calculate_taxes_and_totals(); + return this.shipping_rule(); } } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index a8cec0ad12a..25df4785c31 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1087,6 +1087,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return this.frm.call({ doc: this.frm.doc, method: "apply_shipping_rule", + callback: function(r) { + me._calculate_taxes_and_totals(); + } }).fail(() => this.frm.set_value('shipping_rule', '')); } } From 33fa14b6cf8acb929ab6e37dd240d866b6b899c8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sun, 27 Mar 2022 19:23:53 +0530 Subject: [PATCH 10/49] fix: Resolve conflicts --- erpnext/public/js/controllers/taxes_and_totals.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index c861ebb19df..6dc3cd7d0a1 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -34,11 +34,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { frappe.model.set_value(item.doctype, item.name, "rate", item_rate); } -<<<<<<< HEAD - calculate_taxes_and_totals(update_paid_amount) { -======= - calculate_taxes_and_totals: async function(update_paid_amount) { ->>>>>>> a044e92687 (fix: Incorrect default amount to pay for POS invoices) + async calculate_taxes_and_totals(update_paid_amount) { this.discount_amount_applied = false; this._calculate_taxes_and_totals(); this.calculate_discount_amount(); From 03952f8819b61c9842f865623f162a96222422d2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Mar 2022 18:47:58 +0530 Subject: [PATCH 11/49] fix(India): Auto tax fetching based on GSTIN (cherry picked from commit 7cae669e814a958310ca1b8ee408450569c32d10) # Conflicts: # erpnext/patches.txt # erpnext/regional/india/setup.py # erpnext/selling/doctype/quotation/quotation.json --- erpnext/patches.txt | 7 + .../create_gst_custom_fields_in_quotation.py | 29 ++ erpnext/regional/india/setup.py | 21 + erpnext/regional/india/utils.py | 6 +- .../selling/doctype/quotation/quotation.json | 392 ++++++++++++++---- .../doctype/quotation/regional/india.js | 3 + 6 files changed, 372 insertions(+), 86 deletions(-) create mode 100644 erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py create mode 100644 erpnext/selling/doctype/quotation/regional/india.js diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 028834a0ec4..83464aec311 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -356,6 +356,7 @@ erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v14_0.delete_amazon_mws_doctype erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs +<<<<<<< HEAD erpnext.patches.v14_0.update_batch_valuation_flag erpnext.patches.v14_0.delete_non_profit_doctypes erpnext.patches.v14_0.update_employee_advance_status @@ -363,3 +364,9 @@ 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.remove_unknown_links_to_prod_plan_items +erpnext.patches.v13_0.rename_non_profit_fields +erpnext.patches.v13_0.enable_ksa_vat_docs #1 +erpnext.patches.v13_0.create_gst_custom_fields_in_quotation +>>>>>>> 7cae669e81 (fix(India): Auto tax fetching based on GSTIN) diff --git a/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py b/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py new file mode 100644 index 00000000000..840b8fd830f --- /dev/null +++ b/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py @@ -0,0 +1,29 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name']) + if not company: + return + + sales_invoice_gst_fields = [ + dict(fieldname='billing_address_gstin', label='Billing Address GSTIN', + fieldtype='Data', insert_after='customer_address', read_only=1, + fetch_from='customer_address.gstin', print_hide=1, length=15), + dict(fieldname='customer_gstin', label='Customer GSTIN', + fieldtype='Data', insert_after='shipping_address_name', + fetch_from='shipping_address_name.gstin', print_hide=1, length=15), + dict(fieldname='place_of_supply', label='Place of Supply', + fieldtype='Data', insert_after='customer_gstin', + print_hide=1, read_only=1, length=50), + dict(fieldname='company_gstin', label='Company GSTIN', + fieldtype='Data', insert_after='company_address', + fetch_from='company_address.gstin', print_hide=1, read_only=1, length=15), + ] + + custom_fields = { + 'Quotation': sales_invoice_gst_fields + } + + create_custom_fields(custom_fields, update=True) \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 40fa6cd097c..a7938e093ca 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -909,6 +909,7 @@ def get_custom_fields(): read_only=1, ), ], +<<<<<<< HEAD "Purchase Invoice": purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields @@ -947,6 +948,26 @@ def get_custom_fields(): dict( fieldname="is_non_gst", label="Is Non GST ", fieldtype="Check", insert_after="is_nil_exempt" ), +======= + 'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields, + 'Purchase Order': purchase_invoice_gst_fields, + 'Purchase Receipt': purchase_invoice_gst_fields, + 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, + 'POS Invoice': sales_invoice_gst_fields, + 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category, + 'Payment Entry': payment_entry_fields, + 'Journal Entry': journal_entry_fields, + 'Sales Order': sales_invoice_gst_fields, + 'Tax Category': inter_state_gst_field, + 'Quotation': sales_invoice_gst_fields, + 'Item': [ + dict(fieldname='gst_hsn_code', label='HSN/SAC', + fieldtype='Link', options='GST HSN Code', insert_after='item_group'), + dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted', + fieldtype='Check', insert_after='gst_hsn_code'), + dict(fieldname='is_non_gst', label='Is Non GST ', + fieldtype='Check', insert_after='is_nil_exempt') +>>>>>>> 7cae669e81 (fix(India): Auto tax fetching based on GSTIN) ], "Quotation Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], "Supplier Quotation Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 47e6ae67f4e..1920b373213 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -225,7 +225,7 @@ def get_place_of_supply(party_details, doctype): if not frappe.get_meta("Address").has_field("gst_state"): return - if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"): address_name = party_details.customer_address or party_details.shipping_address_name elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): address_name = party_details.shipping_address or party_details.supplier_address @@ -254,7 +254,7 @@ def get_regional_address_details(party_details, doctype, company): party_details.taxes = [] return party_details - if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"): master_doctype = "Sales Taxes and Charges Template" tax_template_by_category = get_tax_template_based_on_category( master_doctype, company, party_details @@ -310,7 +310,7 @@ def update_party_details(party_details, doctype): def is_internal_transfer(party_details, doctype): - if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"): destination_gstin = party_details.company_gstin elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): destination_gstin = party_details.supplier_gstin diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index ee5b0ea760a..018682ff80e 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -31,6 +31,8 @@ "col_break98", "shipping_address_name", "shipping_address", + "company_address", + "company_address_display", "customer_group", "territory", "currency_and_price_list", @@ -117,7 +119,9 @@ { "fieldname": "customer_section", "fieldtype": "Section Break", - "options": "fa fa-user" + "options": "fa fa-user", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -127,7 +131,9 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "naming_series", @@ -139,7 +145,9 @@ "options": "SAL-QTN-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "Customer", @@ -151,7 +159,9 @@ "oldfieldtype": "Select", "options": "DocType", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -164,7 +174,9 @@ "oldfieldtype": "Link", "options": "quotation_to", "print_hide": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -173,12 +185,16 @@ "hidden": 1, "in_global_search": 1, "label": "Customer Name", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -192,6 +208,8 @@ "options": "Quotation", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "150px" }, { @@ -204,6 +222,8 @@ "print_hide": 1, "remember_last_selected_value": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "150px" }, { @@ -218,12 +238,16 @@ "oldfieldtype": "Date", "reqd": 1, "search_index": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { "fieldname": "valid_till", "fieldtype": "Date", - "label": "Valid Till" + "label": "Valid Till", + "show_days": 1, + "show_seconds": 1 }, { "default": "Sales", @@ -235,7 +259,9 @@ "oldfieldtype": "Select", "options": "\nSales\nMaintenance\nShopping Cart", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -243,14 +269,18 @@ "fieldname": "contact_section", "fieldtype": "Section Break", "label": "Address and Contact", - "options": "fa fa-bullhorn" + "options": "fa fa-bullhorn", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_address", "fieldtype": "Link", "label": "Customer Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "address_display", @@ -258,7 +288,9 @@ "label": "Address", "oldfieldname": "customer_address", "oldfieldtype": "Small Text", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_person", @@ -267,20 +299,26 @@ "oldfieldname": "contact_person", "oldfieldtype": "Link", "options": "Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "in_global_search": 1, "label": "Contact", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_email", @@ -289,12 +327,16 @@ "label": "Contact Email", "options": "Email", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", "fieldname": "col_break98", "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -302,14 +344,18 @@ "fieldtype": "Link", "label": "Shipping Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", @@ -320,21 +366,27 @@ "oldfieldname": "customer_group", "oldfieldtype": "Link", "options": "Customer Group", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "territory", "fieldtype": "Link", "label": "Territory", "options": "Territory", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", "label": "Currency and Price List", - "options": "fa fa-tag" + "options": "fa fa-tag", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "currency", @@ -345,6 +397,8 @@ "options": "Currency", "print_hide": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -357,11 +411,15 @@ "precision": "9", "print_hide": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { "fieldname": "column_break2", "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -373,6 +431,8 @@ "options": "Price List", "print_hide": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -382,7 +442,9 @@ "options": "Currency", "print_hide": 1, "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "Rate at which Price list currency is converted to company's base currency", @@ -391,7 +453,9 @@ "label": "Price List Exchange Rate", "precision": "9", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -400,13 +464,17 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "items_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart" + "options": "fa fa-shopping-cart", + "show_days": 1, + "show_seconds": 1 }, { "allow_bulk_edit": 1, @@ -417,29 +485,39 @@ "oldfieldtype": "Table", "options": "Quotation Item", "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "40px" }, { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", - "label": "Pricing Rules" + "label": "Pricing Rules", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_break23", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Quantity", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total", @@ -447,7 +525,9 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_net_total", @@ -458,18 +538,24 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { "fieldname": "column_break_28", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total", "fieldtype": "Currency", "label": "Total", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "net_total", @@ -477,32 +563,42 @@ "label": "Net Total", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_section", "fieldtype": "Section Break", "label": "Taxes and Charges", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", "options": "Tax Category", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_34", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_rule", @@ -510,11 +606,15 @@ "label": "Shipping Rule", "oldfieldtype": "Button", "options": "Shipping Rule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_36", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges", @@ -523,7 +623,9 @@ "oldfieldname": "charge", "oldfieldtype": "Link", "options": "Sales Taxes and Charges Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", @@ -531,13 +633,17 @@ "label": "Sales Taxes and Charges", "oldfieldname": "other_charges", "oldfieldtype": "Table", - "options": "Sales Taxes and Charges" + "options": "Sales Taxes and Charges", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", - "label": "Tax Breakup" + "label": "Tax Breakup", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "other_charges_calculation", @@ -546,11 +652,15 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_39", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -560,11 +670,15 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_42", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", @@ -572,26 +686,34 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "discount_amount", "fieldname": "section_break_44", "fieldtype": "Section Break", - "label": "Additional Discount and Coupon Code" + "label": "Additional Discount and Coupon Code", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "coupon_code", "fieldtype": "Link", "label": "Coupon Code", - "options": "Coupon Code" + "options": "Coupon Code", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "referral_sales_partner", "fieldtype": "Link", "label": "Referral Sales Partner", - "options": "Sales Partner" + "options": "Sales Partner", + "show_days": 1, + "show_seconds": 1 }, { "default": "Grand Total", @@ -599,7 +721,9 @@ "fieldtype": "Select", "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_discount_amount", @@ -607,31 +731,41 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_46", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", "label": "Additional Discount Percentage", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_grand_total", @@ -642,6 +776,8 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { @@ -651,7 +787,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "In Words will be visible once you save the Quotation.", @@ -663,6 +801,8 @@ "oldfieldtype": "Data", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { @@ -674,6 +814,8 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { @@ -681,6 +823,8 @@ "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -692,6 +836,8 @@ "oldfieldtype": "Currency", "options": "currency", "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { @@ -701,7 +847,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -712,6 +860,8 @@ "oldfieldtype": "Currency", "options": "currency", "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { @@ -723,19 +873,25 @@ "oldfieldtype": "Data", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { "fieldname": "payment_schedule_section", "fieldtype": "Section Break", - "label": "Payment Terms" + "label": "Payment Terms", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_terms_template", "fieldtype": "Link", "label": "Payment Terms Template", "options": "Payment Terms Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_schedule", @@ -743,7 +899,9 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -752,7 +910,9 @@ "fieldtype": "Section Break", "label": "Terms and Conditions", "oldfieldtype": "Section Break", - "options": "fa fa-legal" + "options": "fa fa-legal", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tc_name", @@ -762,20 +922,26 @@ "oldfieldtype": "Link", "options": "Terms and Conditions", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", "label": "Term Details", "oldfieldname": "terms", - "oldfieldtype": "Text Editor" + "oldfieldtype": "Text Editor", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "print_settings", "fieldtype": "Section Break", - "label": "Print Settings" + "label": "Print Settings", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -785,7 +951,9 @@ "oldfieldname": "letter_head", "oldfieldtype": "Select", "options": "Letter Head", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -793,11 +961,15 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_73", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -809,19 +981,25 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "subscription_section", "fieldtype": "Section Break", - "label": "Auto Repeat Section" + "label": "Auto Repeat Section", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "auto_repeat", @@ -830,14 +1008,18 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", - "label": "Update Auto Repeat Reference" + "label": "Update Auto Repeat Reference", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -846,7 +1028,9 @@ "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "campaign", @@ -855,7 +1039,9 @@ "oldfieldname": "campaign", "oldfieldtype": "Link", "options": "Campaign", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "source", @@ -864,7 +1050,9 @@ "oldfieldname": "source", "oldfieldtype": "Select", "options": "Lead Source", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -875,13 +1063,17 @@ "no_copy": 1, "oldfieldname": "order_lost_reason", "oldfieldtype": "Small Text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break4", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -896,7 +1088,9 @@ "options": "Draft\nOpen\nReplied\nOrdered\nLost\nCancelled\nExpired", "print_hide": 1, "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "enq_det", @@ -906,13 +1100,17 @@ "oldfieldname": "enq_det", "oldfieldtype": "Text", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier_quotation", "fieldtype": "Link", "label": "Supplier Quotation", - "options": "Supplier Quotation" + "options": "Supplier Quotation", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "opportunity", @@ -920,7 +1118,9 @@ "label": "Opportunity", "options": "Opportunity", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -928,7 +1128,9 @@ "fieldtype": "Table MultiSelect", "label": "Lost Reasons", "options": "Quotation Lost Reason Detail", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "packed_items", @@ -936,7 +1138,9 @@ "fieldtype": "Table", "label": "Bundle Items", "options": "Packed Item", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -946,6 +1150,7 @@ "fieldtype": "Section Break", "label": "Bundle Items", "options": "fa fa-suitcase", +<<<<<<< HEAD "print_hide": 1 }, { @@ -955,13 +1160,34 @@ "label": "Competitors", "options": "Competitor Detail", "read_only": 1 +======= + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "company_address", + "fieldtype": "Link", + "label": "Company Address Name", + "options": "Address", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "company_address_display", + "fieldtype": "Small Text", + "label": "Company Address", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 +>>>>>>> 7cae669e81 (fix(India): Auto tax fetching based on GSTIN) } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2021-11-30 01:33:21.106073", + "modified": "2022-03-23 16:49:36.297403", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/regional/india.js b/erpnext/selling/doctype/quotation/regional/india.js new file mode 100644 index 00000000000..955083565bc --- /dev/null +++ b/erpnext/selling/doctype/quotation/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Quotation'); From e57e7bb02ca1713ab63259d97a2f879bb5f40991 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Sat, 2 Apr 2022 17:16:56 +0530 Subject: [PATCH 12/49] 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 cba6759f615..bc04b772c61 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 21fa9c9ab41..7ef16d76fd4 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -35,7 +35,6 @@ class CallLog(Document): # Add Employee Name if self.is_incoming_call(): # Taking the last 10 digits of the number - employee_number = strip_number(self.get("to")) employee = get_employees_with_number(self.get("to")) self.call_received_by = employee[0].get("name") diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py new file mode 100644 index 00000000000..91e8f50c3cb --- /dev/null +++ b/erpnext/tests/test_exotel.py @@ -0,0 +1,199 @@ +import os +import time +import unittest + +import frappe +import requests +from frappe.contacts.doctype.contact.test_contact import create_contact + +from erpnext.hr.doctype.employee.test_employee import make_employee + + +class TestExotel(unittest.TestCase): + def setUp(self): + make_employee("test_employee_exotel@company.com", cell_number="9999999999") + phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}] + create_contact("Test Contact", "Mr", phones=phones) + + def test_for_successful_call(self): + data = { + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "Created": "Wed, 23 Feb 2022 12:31:59", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-23 12:32:02", + "DialWhomNumber": "09999999999", + "Status": "busy", + "EventType": "Dial", + "AgentEmail": "test_employee_exotel@company.com", + } + end_call_data = { + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Wed, 23 Feb 2022 12:31:59", + "DialCallDuration": "17", + "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/exotelrecordings/erpnext/23c162077629863c1a2d7f29263a162n.mp3", + "StartTime": "2022-02-23 12:31:58", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "completed", + "CallType": "completed", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", + "CurrentTime": "2022-02-23 12:32:25", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "10", + "CallerId": "09999999980", + "CauseCode": "NORMAL_CLEARING", + "Cause": "16", + } + ], + } + api_method = "handle_incoming_call" + end_call_api_method = "handle_end_call" + emulate_api_call(data, api_method, end_call_data, end_call_api_method) + + frappe.reload_doctype("Call Log") + call_log = frappe.get_doc( + "Call Log", {"from": "09999999991", "to": "09999999999", "status": "Completed"} + ) + + self.assertEqual(call_log.get("from"), "09999999991") + self.assertEqual(call_log.get("to"), "09999999999") + self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("status"), "Completed") + + def test_for_disconnected_call(self): + data = { + "CallSid": "d96421addce69e24bdc7ce5880d1162l", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:58:12", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:58:12", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "canceled", + "CallType": "client-hangup", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:58:47", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], + } + api_method = "handle_missed_call" + emulate_api_call(data, api_method) + + frappe.reload_doctype("Call Log") + call_log = frappe.get_doc( + "Call Log", {"from": "09999999991", "to": "09999999999", "status": "Canceled"} + ) + + self.assertEqual(call_log.get("from"), "09999999991") + self.assertEqual(call_log.get("to"), "09999999999") + self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("status"), "Canceled") + + def test_for_call_not_answered(self): + data = { + "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:47:02", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:47:02", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "no-answer", + "CallType": "incomplete", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:47:40", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], + } + api_method = "handle_missed_call" + emulate_api_call(data, api_method) + + frappe.reload_doctype("Call Log") + call_log = frappe.get_doc( + "Call Log", {"from": "09999999991", "to": "09999999999", "status": "No Answer"} + ) + + self.assertEqual(call_log.get("from"), "09999999991") + self.assertEqual(call_log.get("to"), "09999999999") + self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("status"), "No Answer") + + def tearDown(self): + frappe.db.rollback() + + +def emulate_api_call(data, api_method, end_call_data=None, end_call_api_method=None): + # Build URL + port = frappe.get_site_config().webserver_port or "8000" + + if os.environ.get("CI"): + host = "localhost" + else: + host = frappe.local.site + + url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}".format( + site=host, port=port, api_method=api_method + ) + + if end_call_data: + end_call_url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{end_call_api_method}".format( + site=host, port=port, end_call_api_method=end_call_api_method + ) + + requests.post(url=url, data=data) + time.sleep(3) + + if end_call_data: + requests.post(url=end_call_url, data=end_call_data) + time.sleep(3) + + return From 485f5cfdfedfe935efbf2be1c089b4a13defd586 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 2 Apr 2022 17:25:29 +0530 Subject: [PATCH 13/49] fix: Resolve conflicts --- erpnext/patches.txt | 8 +- erpnext/regional/india/setup.py | 22 +- .../selling/doctype/quotation/quotation.json | 394 ++++-------------- 3 files changed, 89 insertions(+), 335 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 83464aec311..a64a72e1a63 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -356,7 +356,6 @@ erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v14_0.delete_amazon_mws_doctype erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs -<<<<<<< HEAD erpnext.patches.v14_0.update_batch_valuation_flag erpnext.patches.v14_0.delete_non_profit_doctypes erpnext.patches.v14_0.update_employee_advance_status @@ -364,9 +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.remove_unknown_links_to_prod_plan_items -erpnext.patches.v13_0.rename_non_profit_fields -erpnext.patches.v13_0.enable_ksa_vat_docs #1 -erpnext.patches.v13_0.create_gst_custom_fields_in_quotation ->>>>>>> 7cae669e81 (fix(India): Auto tax fetching based on GSTIN) +erpnext.patches.v13_0.create_gst_custom_fields_in_quotation \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index a7938e093ca..446faaa708b 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -909,7 +909,6 @@ def get_custom_fields(): read_only=1, ), ], -<<<<<<< HEAD "Purchase Invoice": purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields @@ -931,6 +930,7 @@ def get_custom_fields(): "Journal Entry": journal_entry_fields, "Sales Order": sales_invoice_gst_fields, "Tax Category": inter_state_gst_field, + "Quotation": sales_invoice_gst_fields, "Item": [ dict( fieldname="gst_hsn_code", @@ -948,26 +948,6 @@ def get_custom_fields(): dict( fieldname="is_non_gst", label="Is Non GST ", fieldtype="Check", insert_after="is_nil_exempt" ), -======= - 'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields, - 'Purchase Order': purchase_invoice_gst_fields, - 'Purchase Receipt': purchase_invoice_gst_fields, - 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, - 'POS Invoice': sales_invoice_gst_fields, - 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category, - 'Payment Entry': payment_entry_fields, - 'Journal Entry': journal_entry_fields, - 'Sales Order': sales_invoice_gst_fields, - 'Tax Category': inter_state_gst_field, - 'Quotation': sales_invoice_gst_fields, - 'Item': [ - dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Link', options='GST HSN Code', insert_after='item_group'), - dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted', - fieldtype='Check', insert_after='gst_hsn_code'), - dict(fieldname='is_non_gst', label='Is Non GST ', - fieldtype='Check', insert_after='is_nil_exempt') ->>>>>>> 7cae669e81 (fix(India): Auto tax fetching based on GSTIN) ], "Quotation Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], "Supplier Quotation Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 018682ff80e..b069e622744 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -112,16 +112,13 @@ "enq_det", "supplier_quotation", "opportunity", - "lost_reasons", - "competitors" + "lost_reasons" ], "fields": [ { "fieldname": "customer_section", "fieldtype": "Section Break", - "options": "fa fa-user", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-user" }, { "allow_on_submit": 1, @@ -131,9 +128,7 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "naming_series", @@ -145,9 +140,7 @@ "options": "SAL-QTN-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1, - "show_days": 1, - "show_seconds": 1 + "set_only_once": 1 }, { "default": "Customer", @@ -159,9 +152,7 @@ "oldfieldtype": "Select", "options": "DocType", "print_hide": 1, - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "bold": 1, @@ -174,9 +165,7 @@ "oldfieldtype": "Link", "options": "quotation_to", "print_hide": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "bold": 1, @@ -185,16 +174,12 @@ "hidden": 1, "in_global_search": 1, "label": "Customer Name", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -208,8 +193,6 @@ "options": "Quotation", "print_hide": 1, "read_only": 1, - "show_days": 1, - "show_seconds": 1, "width": "150px" }, { @@ -222,8 +205,6 @@ "print_hide": 1, "remember_last_selected_value": 1, "reqd": 1, - "show_days": 1, - "show_seconds": 1, "width": "150px" }, { @@ -238,16 +219,12 @@ "oldfieldtype": "Date", "reqd": 1, "search_index": 1, - "show_days": 1, - "show_seconds": 1, "width": "100px" }, { "fieldname": "valid_till", "fieldtype": "Date", - "label": "Valid Till", - "show_days": 1, - "show_seconds": 1 + "label": "Valid Till" }, { "default": "Sales", @@ -259,9 +236,7 @@ "oldfieldtype": "Select", "options": "\nSales\nMaintenance\nShopping Cart", "print_hide": 1, - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "collapsible": 1, @@ -269,18 +244,14 @@ "fieldname": "contact_section", "fieldtype": "Section Break", "label": "Address and Contact", - "options": "fa fa-bullhorn", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-bullhorn" }, { "fieldname": "customer_address", "fieldtype": "Link", "label": "Customer Address", "options": "Address", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "address_display", @@ -288,9 +259,7 @@ "label": "Address", "oldfieldname": "customer_address", "oldfieldtype": "Small Text", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_person", @@ -299,26 +268,20 @@ "oldfieldname": "contact_person", "oldfieldtype": "Link", "options": "Contact", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "in_global_search": 1, "label": "Contact", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_email", @@ -327,16 +290,12 @@ "label": "Contact Email", "options": "Email", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", "fieldname": "col_break98", "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -344,18 +303,14 @@ "fieldtype": "Link", "label": "Shipping Address", "options": "Address", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "shipping_address", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", @@ -366,27 +321,21 @@ "oldfieldname": "customer_group", "oldfieldtype": "Link", "options": "Customer Group", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "territory", "fieldtype": "Link", "label": "Territory", "options": "Territory", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", "label": "Currency and Price List", - "options": "fa fa-tag", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tag" }, { "fieldname": "currency", @@ -397,8 +346,6 @@ "options": "Currency", "print_hide": 1, "reqd": 1, - "show_days": 1, - "show_seconds": 1, "width": "100px" }, { @@ -411,15 +358,11 @@ "precision": "9", "print_hide": 1, "reqd": 1, - "show_days": 1, - "show_seconds": 1, "width": "100px" }, { "fieldname": "column_break2", "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -431,8 +374,6 @@ "options": "Price List", "print_hide": 1, "reqd": 1, - "show_days": 1, - "show_seconds": 1, "width": "100px" }, { @@ -442,9 +383,7 @@ "options": "Currency", "print_hide": 1, "read_only": 1, - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "description": "Rate at which Price list currency is converted to company's base currency", @@ -453,9 +392,7 @@ "label": "Price List Exchange Rate", "precision": "9", "print_hide": 1, - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "default": "0", @@ -464,17 +401,13 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "items_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-shopping-cart" }, { "allow_bulk_edit": 1, @@ -485,39 +418,29 @@ "oldfieldtype": "Table", "options": "Quotation Item", "reqd": 1, - "show_days": 1, - "show_seconds": 1, "width": "40px" }, { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", - "label": "Pricing Rules", - "show_days": 1, - "show_seconds": 1 + "label": "Pricing Rules" }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "sec_break23", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Quantity", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_total", @@ -525,9 +448,7 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_net_total", @@ -538,24 +459,18 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1, - "show_days": 1, - "show_seconds": 1, "width": "100px" }, { "fieldname": "column_break_28", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "total", "fieldtype": "Currency", "label": "Total", "options": "currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "net_total", @@ -563,42 +478,32 @@ "label": "Net Total", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "taxes_section", "fieldtype": "Section Break", "label": "Taxes and Charges", "oldfieldtype": "Section Break", - "options": "fa fa-money", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-money" }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", "options": "Tax Category", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_34", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "shipping_rule", @@ -606,15 +511,11 @@ "label": "Shipping Rule", "oldfieldtype": "Button", "options": "Shipping Rule", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "section_break_36", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "taxes_and_charges", @@ -623,9 +524,7 @@ "oldfieldname": "charge", "oldfieldtype": "Link", "options": "Sales Taxes and Charges Template", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "taxes", @@ -633,17 +532,13 @@ "label": "Sales Taxes and Charges", "oldfieldname": "other_charges", "oldfieldtype": "Table", - "options": "Sales Taxes and Charges", - "show_days": 1, - "show_seconds": 1 + "options": "Sales Taxes and Charges" }, { "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", - "label": "Tax Breakup", - "show_days": 1, - "show_seconds": 1 + "label": "Tax Breakup" }, { "fieldname": "other_charges_calculation", @@ -652,15 +547,11 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "section_break_39", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "base_total_taxes_and_charges", @@ -670,15 +561,11 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_42", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "total_taxes_and_charges", @@ -686,34 +573,26 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "collapsible_depends_on": "discount_amount", "fieldname": "section_break_44", "fieldtype": "Section Break", - "label": "Additional Discount and Coupon Code", - "show_days": 1, - "show_seconds": 1 + "label": "Additional Discount and Coupon Code" }, { "fieldname": "coupon_code", "fieldtype": "Link", "label": "Coupon Code", - "options": "Coupon Code", - "show_days": 1, - "show_seconds": 1 + "options": "Coupon Code" }, { "fieldname": "referral_sales_partner", "fieldtype": "Link", "label": "Referral Sales Partner", - "options": "Sales Partner", - "show_days": 1, - "show_seconds": 1 + "options": "Sales Partner" }, { "default": "Grand Total", @@ -721,9 +600,7 @@ "fieldtype": "Select", "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "base_discount_amount", @@ -731,41 +608,31 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_46", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", "label": "Additional Discount Percentage", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "base_grand_total", @@ -776,8 +643,6 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1, - "show_days": 1, - "show_seconds": 1, "width": "200px" }, { @@ -787,9 +652,7 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "description": "In Words will be visible once you save the Quotation.", @@ -801,8 +664,6 @@ "oldfieldtype": "Data", "print_hide": 1, "read_only": 1, - "show_days": 1, - "show_seconds": 1, "width": "200px" }, { @@ -814,8 +675,6 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1, - "show_days": 1, - "show_seconds": 1, "width": "200px" }, { @@ -823,8 +682,6 @@ "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -836,8 +693,6 @@ "oldfieldtype": "Currency", "options": "currency", "read_only": 1, - "show_days": 1, - "show_seconds": 1, "width": "200px" }, { @@ -847,9 +702,7 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "bold": 1, @@ -860,8 +713,6 @@ "oldfieldtype": "Currency", "options": "currency", "read_only": 1, - "show_days": 1, - "show_seconds": 1, "width": "200px" }, { @@ -873,25 +724,19 @@ "oldfieldtype": "Data", "print_hide": 1, "read_only": 1, - "show_days": 1, - "show_seconds": 1, "width": "200px" }, { "fieldname": "payment_schedule_section", "fieldtype": "Section Break", - "label": "Payment Terms", - "show_days": 1, - "show_seconds": 1 + "label": "Payment Terms" }, { "fieldname": "payment_terms_template", "fieldtype": "Link", "label": "Payment Terms Template", "options": "Payment Terms Template", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "payment_schedule", @@ -899,9 +744,7 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, @@ -910,9 +753,7 @@ "fieldtype": "Section Break", "label": "Terms and Conditions", "oldfieldtype": "Section Break", - "options": "fa fa-legal", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-legal" }, { "fieldname": "tc_name", @@ -922,26 +763,20 @@ "oldfieldtype": "Link", "options": "Terms and Conditions", "print_hide": 1, - "report_hide": 1, - "show_days": 1, - "show_seconds": 1 + "report_hide": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", "label": "Term Details", "oldfieldname": "terms", - "oldfieldtype": "Text Editor", - "show_days": 1, - "show_seconds": 1 + "oldfieldtype": "Text Editor" }, { "collapsible": 1, "fieldname": "print_settings", "fieldtype": "Section Break", - "label": "Print Settings", - "show_days": 1, - "show_seconds": 1 + "label": "Print Settings" }, { "allow_on_submit": 1, @@ -951,9 +786,7 @@ "oldfieldname": "letter_head", "oldfieldtype": "Select", "options": "Letter Head", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -961,15 +794,11 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_73", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "allow_on_submit": 1, @@ -981,25 +810,19 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1, - "show_days": 1, - "show_seconds": 1 + "report_hide": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "subscription_section", "fieldtype": "Section Break", - "label": "Auto Repeat Section", - "show_days": 1, - "show_seconds": 1 + "label": "Auto Repeat Section" }, { "fieldname": "auto_repeat", @@ -1008,18 +831,14 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "allow_on_submit": 1, "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", - "label": "Update Auto Repeat Reference", - "show_days": 1, - "show_seconds": 1 + "label": "Update Auto Repeat Reference" }, { "collapsible": 1, @@ -1028,9 +847,7 @@ "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "campaign", @@ -1039,9 +856,7 @@ "oldfieldname": "campaign", "oldfieldtype": "Link", "options": "Campaign", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "source", @@ -1050,9 +865,7 @@ "oldfieldname": "source", "oldfieldtype": "Select", "options": "Lead Source", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -1063,17 +876,13 @@ "no_copy": 1, "oldfieldname": "order_lost_reason", "oldfieldtype": "Small Text", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break4", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -1088,9 +897,7 @@ "options": "Draft\nOpen\nReplied\nOrdered\nLost\nCancelled\nExpired", "print_hide": 1, "read_only": 1, - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "enq_det", @@ -1100,17 +907,13 @@ "oldfieldname": "enq_det", "oldfieldtype": "Text", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "supplier_quotation", "fieldtype": "Link", "label": "Supplier Quotation", - "options": "Supplier Quotation", - "show_days": 1, - "show_seconds": 1 + "options": "Supplier Quotation" }, { "fieldname": "opportunity", @@ -1118,9 +921,7 @@ "label": "Opportunity", "options": "Opportunity", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "allow_on_submit": 1, @@ -1128,9 +929,7 @@ "fieldtype": "Table MultiSelect", "label": "Lost Reasons", "options": "Quotation Lost Reason Detail", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "packed_items", @@ -1138,9 +937,7 @@ "fieldtype": "Table", "label": "Bundle Items", "options": "Packed Item", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, @@ -1150,44 +947,26 @@ "fieldtype": "Section Break", "label": "Bundle Items", "options": "fa fa-suitcase", -<<<<<<< HEAD "print_hide": 1 }, - { - "allow_on_submit": 1, - "fieldname": "competitors", - "fieldtype": "Table MultiSelect", - "label": "Competitors", - "options": "Competitor Detail", - "read_only": 1 -======= - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 - }, { "fieldname": "company_address", "fieldtype": "Link", "label": "Company Address Name", - "options": "Address", - "show_days": 1, - "show_seconds": 1 + "options": "Address" }, { "fieldname": "company_address_display", "fieldtype": "Small Text", "label": "Company Address", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 ->>>>>>> 7cae669e81 (fix(India): Auto tax fetching based on GSTIN) + "read_only": 1 } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2022-03-23 16:49:36.297403", + "modified": "2022-04-02 17:21:48.578719", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", @@ -1282,6 +1061,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "party_name", "title_field": "title" } \ No newline at end of file From c58fde2fb262bf2fa15eea72cecc7616e3943ca6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 2 Apr 2022 20:03:46 +0530 Subject: [PATCH 14/49] fix: Add competitor field back --- erpnext/selling/doctype/quotation/quotation.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index b069e622744..b5976542215 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -112,7 +112,8 @@ "enq_det", "supplier_quotation", "opportunity", - "lost_reasons" + "lost_reasons", + "competitors" ], "fields": [ { @@ -949,6 +950,13 @@ "options": "fa fa-suitcase", "print_hide": 1 }, + { + "allow_on_submit": 1, + "fieldname": "competitors", + "fieldtype": "Table MultiSelect", + "label": "Competitors", + "options": "Competitor Detail" + }, { "fieldname": "company_address", "fieldtype": "Link", @@ -966,7 +974,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "modified": "2022-04-02 17:21:48.578719", + "modified": "2022-04-03 17:21:48.578719", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", From a7e125a54083be62ba8f80dee0542360140888f8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 2 Apr 2022 20:36:36 +0530 Subject: [PATCH 15/49] chore: format patch file --- .../create_gst_custom_fields_in_quotation.py | 60 +++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py b/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py index 840b8fd830f..3217eab43d6 100644 --- a/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py +++ b/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py @@ -3,27 +3,51 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name']) + company = frappe.get_all("Company", filters={"country": "India"}, fields=["name"]) if not company: return sales_invoice_gst_fields = [ - dict(fieldname='billing_address_gstin', label='Billing Address GSTIN', - fieldtype='Data', insert_after='customer_address', read_only=1, - fetch_from='customer_address.gstin', print_hide=1, length=15), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='shipping_address_name', - fetch_from='shipping_address_name.gstin', print_hide=1, length=15), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='customer_gstin', - print_hide=1, read_only=1, length=50), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1, length=15), - ] + dict( + fieldname="billing_address_gstin", + label="Billing Address GSTIN", + fieldtype="Data", + insert_after="customer_address", + read_only=1, + fetch_from="customer_address.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="customer_gstin", + label="Customer GSTIN", + fieldtype="Data", + insert_after="shipping_address_name", + fetch_from="shipping_address_name.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="customer_gstin", + print_hide=1, + read_only=1, + length=50, + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="company_address", + fetch_from="company_address.gstin", + print_hide=1, + read_only=1, + length=15, + ), + ] - custom_fields = { - 'Quotation': sales_invoice_gst_fields - } + custom_fields = {"Quotation": sales_invoice_gst_fields} - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) From 0e9ebad9c6bc125ffbd0c23403a3601087282453 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 07:23:08 +0530 Subject: [PATCH 16/49] test: Refactor exotel test setup - Remove unnecessary code - Move test data to separate file - Make proper test assertions --- erpnext/tests/exotel_test_data.py | 114 +++++++++++++++++++ erpnext/tests/test_exotel.py | 179 +++++------------------------- 2 files changed, 140 insertions(+), 153 deletions(-) create mode 100644 erpnext/tests/exotel_test_data.py diff --git a/erpnext/tests/exotel_test_data.py b/erpnext/tests/exotel_test_data.py new file mode 100644 index 00000000000..e2ae7f776ea --- /dev/null +++ b/erpnext/tests/exotel_test_data.py @@ -0,0 +1,114 @@ +import frappe + +call_initiation_data = frappe._dict({ + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "Created": "Wed, 23 Feb 2022 12:31:59", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-23 12:32:02", + "DialWhomNumber": "09999999999", + "Status": "busy", + "EventType": "Dial", + "AgentEmail": "test_employee_exotel@company.com", +}) + +call_end_data = frappe._dict({ + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Wed, 23 Feb 2022 12:31:59", + "DialCallDuration": "17", + "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3", + "StartTime": "2022-02-23 12:31:58", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "completed", + "CallType": "completed", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", + "CurrentTime": "2022-02-23 12:32:25", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "10", + "CallerId": "09999999980", + "CauseCode": "NORMAL_CLEARING", + "Cause": "16", + } + ], +}) + +call_disconnected_data = frappe._dict({ + "CallSid": "d96421addce69e24bdc7ce5880d1162l", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:58:12", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:58:12", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "canceled", + "CallType": "client-hangup", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:58:47", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], +}) + +call_not_answered_data = frappe._dict({ + "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:47:02", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:47:02", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "no-answer", + "CallType": "incomplete", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:47:40", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], +}) \ No newline at end of file diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index 91e8f50c3cb..d2e317ac6c6 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -16,154 +16,42 @@ class TestExotel(unittest.TestCase): create_contact("Test Contact", "Mr", phones=phones) def test_for_successful_call(self): - data = { - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "Created": "Wed, 23 Feb 2022 12:31:59", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-23 12:32:02", - "DialWhomNumber": "09999999999", - "Status": "busy", - "EventType": "Dial", - "AgentEmail": "test_employee_exotel@company.com", - } - end_call_data = { - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Wed, 23 Feb 2022 12:31:59", - "DialCallDuration": "17", - "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/exotelrecordings/erpnext/23c162077629863c1a2d7f29263a162n.mp3", - "StartTime": "2022-02-23 12:31:58", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "completed", - "CallType": "completed", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", - "CurrentTime": "2022-02-23 12:32:25", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "10", - "CallerId": "09999999980", - "CauseCode": "NORMAL_CLEARING", - "Cause": "16", - } - ], - } + from .exotel_test_data import call_initiation_data, call_end_data api_method = "handle_incoming_call" end_call_api_method = "handle_end_call" - emulate_api_call(data, api_method, end_call_data, end_call_api_method) - frappe.reload_doctype("Call Log") - call_log = frappe.get_doc( - "Call Log", {"from": "09999999991", "to": "09999999999", "status": "Completed"} - ) + emulate_api_call(call_initiation_data, api_method) + emulate_api_call(call_end_data, end_call_api_method) self.assertEqual(call_log.get("from"), "09999999991") self.assertEqual(call_log.get("to"), "09999999999") + call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid) + + self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom) + self.assertEqual(call_log.get("to"), call_initiation_data.DialWhomNumber) self.assertEqual(call_log.get("call_received_by"), "EMP-00001") self.assertEqual(call_log.get("status"), "Completed") def test_for_disconnected_call(self): - data = { - "CallSid": "d96421addce69e24bdc7ce5880d1162l", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:58:12", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:58:12", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "canceled", - "CallType": "client-hangup", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:58:47", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], - } + from .exotel_test_data import call_disconnected_data api_method = "handle_missed_call" - emulate_api_call(data, api_method) + emulate_api_call(call_disconnected_data, api_method) + call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid) - frappe.reload_doctype("Call Log") - call_log = frappe.get_doc( - "Call Log", {"from": "09999999991", "to": "09999999999", "status": "Canceled"} - ) - - self.assertEqual(call_log.get("from"), "09999999991") - self.assertEqual(call_log.get("to"), "09999999999") + self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom) + self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber) self.assertEqual(call_log.get("call_received_by"), "EMP-00001") self.assertEqual(call_log.get("status"), "Canceled") def test_for_call_not_answered(self): - data = { - "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:47:02", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:47:02", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "no-answer", - "CallType": "incomplete", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:47:40", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], - } + from .exotel_test_data import call_not_answered_data api_method = "handle_missed_call" - emulate_api_call(data, api_method) + emulate_api_call(call_not_answered_data, api_method) - frappe.reload_doctype("Call Log") - call_log = frappe.get_doc( - "Call Log", {"from": "09999999991", "to": "09999999999", "status": "No Answer"} - ) + call_log = frappe.get_doc("Call Log", call_not_answered_data.CallSid) - self.assertEqual(call_log.get("from"), "09999999991") - self.assertEqual(call_log.get("to"), "09999999999") + self.assertEqual(call_log.get("from"), call_not_answered_data.CallFrom) + self.assertEqual(call_log.get("to"), call_not_answered_data.DialWhomNumber) self.assertEqual(call_log.get("call_received_by"), "EMP-00001") self.assertEqual(call_log.get("status"), "No Answer") @@ -171,29 +59,14 @@ class TestExotel(unittest.TestCase): frappe.db.rollback() -def emulate_api_call(data, api_method, end_call_data=None, end_call_api_method=None): +def emulate_api_call(data, api_method): # Build URL + url = get_exotel_handler_endpoint(api_method) + res = requests.post(url=url, data=frappe.as_json(data)) + res.raise_for_status() + time.sleep(1) + +def get_exotel_handler_endpoint(method): + site = "localhost" if os.environ.get("CI") else frappe.local.site port = frappe.get_site_config().webserver_port or "8000" - - if os.environ.get("CI"): - host = "localhost" - else: - host = frappe.local.site - - url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}".format( - site=host, port=port, api_method=api_method - ) - - if end_call_data: - end_call_url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{end_call_api_method}".format( - site=host, port=port, end_call_api_method=end_call_api_method - ) - - requests.post(url=url, data=data) - time.sleep(3) - - if end_call_data: - requests.post(url=end_call_url, data=end_call_data) - time.sleep(3) - - return + return f"http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{method}" \ No newline at end of file From 39abfae5feb7ccbcdc22d5af75fe6ebe97820ffe Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 07:28:41 +0530 Subject: [PATCH 17/49] 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 82bcb8d0dba..522de9ead83 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 7ef16d76fd4..e7fe7abfe9b 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 18/49] 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 e7fe7abfe9b..2092ec284b5 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 19/49] test: Fix erroneous code --- erpnext/tests/exotel_test_data.py | 226 ++++++++++++++++-------------- erpnext/tests/test_exotel.py | 12 +- 2 files changed, 124 insertions(+), 114 deletions(-) diff --git a/erpnext/tests/exotel_test_data.py b/erpnext/tests/exotel_test_data.py index e2ae7f776ea..3ad2575c23d 100644 --- a/erpnext/tests/exotel_test_data.py +++ b/erpnext/tests/exotel_test_data.py @@ -1,114 +1,122 @@ import frappe -call_initiation_data = frappe._dict({ - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "Created": "Wed, 23 Feb 2022 12:31:59", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-23 12:32:02", - "DialWhomNumber": "09999999999", - "Status": "busy", - "EventType": "Dial", - "AgentEmail": "test_employee_exotel@company.com", -}) +call_initiation_data = frappe._dict( + { + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "Created": "Wed, 23 Feb 2022 12:31:59", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-23 12:32:02", + "DialWhomNumber": "09999999999", + "Status": "busy", + "EventType": "Dial", + "AgentEmail": "test_employee_exotel@company.com", + } +) -call_end_data = frappe._dict({ - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Wed, 23 Feb 2022 12:31:59", - "DialCallDuration": "17", - "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3", - "StartTime": "2022-02-23 12:31:58", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "completed", - "CallType": "completed", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", - "CurrentTime": "2022-02-23 12:32:25", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "10", - "CallerId": "09999999980", - "CauseCode": "NORMAL_CLEARING", - "Cause": "16", - } - ], -}) +call_end_data = frappe._dict( + { + "CallSid": "23c162077629863c1a2d7f29263a162m", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Wed, 23 Feb 2022 12:31:59", + "DialCallDuration": "17", + "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3", + "StartTime": "2022-02-23 12:31:58", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "completed", + "CallType": "completed", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", + "CurrentTime": "2022-02-23 12:32:25", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "10", + "CallerId": "09999999980", + "CauseCode": "NORMAL_CLEARING", + "Cause": "16", + } + ], + } +) -call_disconnected_data = frappe._dict({ - "CallSid": "d96421addce69e24bdc7ce5880d1162l", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:58:12", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:58:12", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "canceled", - "CallType": "client-hangup", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:58:47", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], -}) +call_disconnected_data = frappe._dict( + { + "CallSid": "d96421addce69e24bdc7ce5880d1162l", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:58:12", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:58:12", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "canceled", + "CallType": "client-hangup", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:58:47", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], + } +) -call_not_answered_data = frappe._dict({ - "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:47:02", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:47:02", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "no-answer", - "CallType": "incomplete", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:47:40", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], -}) \ No newline at end of file +call_not_answered_data = frappe._dict( + { + "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", + "CallFrom": "09999999991", + "CallTo": "09999999980", + "Direction": "incoming", + "ForwardedFrom": "null", + "Created": "Mon, 21 Feb 2022 15:47:02", + "DialCallDuration": "0", + "StartTime": "2022-02-21 15:47:02", + "EndTime": "1970-01-01 05:30:00", + "DialCallStatus": "no-answer", + "CallType": "incomplete", + "DialWhomNumber": "09999999999", + "ProcessStatus": "null", + "flow_id": "228040", + "tenant_id": "67291", + "From": "09999999991", + "To": "09999999988", + "CurrentTime": "2022-02-21 15:47:40", + "OutgoingPhoneNumber": "09999999988", + "Legs": [ + { + "Number": "09999999999", + "Type": "single", + "OnCallDuration": "0", + "CallerId": "09999999980", + "CauseCode": "RING_TIMEOUT", + "Cause": "1003", + } + ], + } +) diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index d2e317ac6c6..a5dc7dd2118 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -16,15 +16,14 @@ class TestExotel(unittest.TestCase): create_contact("Test Contact", "Mr", phones=phones) def test_for_successful_call(self): - from .exotel_test_data import call_initiation_data, call_end_data + from .exotel_test_data import call_end_data, call_initiation_data + api_method = "handle_incoming_call" end_call_api_method = "handle_end_call" - emulate_api_call(call_initiation_data, api_method) emulate_api_call(call_end_data, end_call_api_method) - self.assertEqual(call_log.get("from"), "09999999991") - self.assertEqual(call_log.get("to"), "09999999999") + call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid) self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom) @@ -34,6 +33,7 @@ class TestExotel(unittest.TestCase): def test_for_disconnected_call(self): from .exotel_test_data import call_disconnected_data + api_method = "handle_missed_call" emulate_api_call(call_disconnected_data, api_method) call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid) @@ -45,6 +45,7 @@ class TestExotel(unittest.TestCase): def test_for_call_not_answered(self): from .exotel_test_data import call_not_answered_data + api_method = "handle_missed_call" emulate_api_call(call_not_answered_data, api_method) @@ -66,7 +67,8 @@ def emulate_api_call(data, api_method): res.raise_for_status() time.sleep(1) + def get_exotel_handler_endpoint(method): site = "localhost" if os.environ.get("CI") else frappe.local.site port = frappe.get_site_config().webserver_port or "8000" - return f"http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{method}" \ No newline at end of file + return f"http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{method}" From 40d33b5fecb0a6418fedb595cfddef2387eb881a Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 11:15:29 +0530 Subject: [PATCH 20/49] test: Use FrappeAPITestCase to track coverage --- erpnext/tests/test_exotel.py | 63 +++++++++++++++++------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index a5dc7dd2118..68c70ea3444 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -1,19 +1,20 @@ -import os -import time -import unittest - import frappe -import requests from frappe.contacts.doctype.contact.test_contact import create_contact +from frappe.tests.test_api import FrappeAPITestCase from erpnext.hr.doctype.employee.test_employee import make_employee -class TestExotel(unittest.TestCase): - def setUp(self): - make_employee("test_employee_exotel@company.com", cell_number="9999999999") +class TestExotel(FrappeAPITestCase): + @classmethod + def setUpClass(cls): + cls.CURRENT_DB_CONNECTION = frappe.db + cls.test_employee_name = make_employee( + user="test_employee_exotel@company.com", cell_number="9999999999" + ) phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}] - create_contact("Test Contact", "Mr", phones=phones) + create_contact(name="Test Contact", salutation="Mr", phones=phones) + frappe.db.commit() def test_for_successful_call(self): from .exotel_test_data import call_end_data, call_initiation_data @@ -21,54 +22,48 @@ class TestExotel(unittest.TestCase): api_method = "handle_incoming_call" end_call_api_method = "handle_end_call" - emulate_api_call(call_initiation_data, api_method) - emulate_api_call(call_end_data, end_call_api_method) - + self.emulate_api_call_from_exotel(api_method, call_initiation_data) + self.emulate_api_call_from_exotel(end_call_api_method, call_end_data) call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid) self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom) self.assertEqual(call_log.get("to"), call_initiation_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) self.assertEqual(call_log.get("status"), "Completed") def test_for_disconnected_call(self): from .exotel_test_data import call_disconnected_data api_method = "handle_missed_call" - emulate_api_call(call_disconnected_data, api_method) + self.emulate_api_call_from_exotel(api_method, call_disconnected_data) call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid) self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom) self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) self.assertEqual(call_log.get("status"), "Canceled") def test_for_call_not_answered(self): from .exotel_test_data import call_not_answered_data api_method = "handle_missed_call" - emulate_api_call(call_not_answered_data, api_method) - + self.emulate_api_call_from_exotel(api_method, call_not_answered_data) call_log = frappe.get_doc("Call Log", call_not_answered_data.CallSid) - self.assertEqual(call_log.get("from"), call_not_answered_data.CallFrom) self.assertEqual(call_log.get("to"), call_not_answered_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), "EMP-00001") + self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) self.assertEqual(call_log.get("status"), "No Answer") - def tearDown(self): - frappe.db.rollback() + def emulate_api_call_from_exotel(self, api_method, data): + self.post( + f"/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}", + data=frappe.as_json(data), + content_type="application/json", + as_tuple=True, + ) + # restart db connection to get latest data + frappe.connect() - -def emulate_api_call(data, api_method): - # Build URL - url = get_exotel_handler_endpoint(api_method) - res = requests.post(url=url, data=frappe.as_json(data)) - res.raise_for_status() - time.sleep(1) - - -def get_exotel_handler_endpoint(method): - site = "localhost" if os.environ.get("CI") else frappe.local.site - port = frappe.get_site_config().webserver_port or "8000" - return f"http://{site}:{port}/api/method/erpnext.erpnext_integrations.exotel_integration.{method}" + @classmethod + def tearDownClass(cls): + frappe.db = cls.CURRENT_DB_CONNECTION From 01909d2e8cb081197fe7c67d9fb049b3930fa6dd Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 4 Apr 2022 20:21:36 +0530 Subject: [PATCH 21/49] test: Fix used frappe._dict to avoid AttributeError down the line ref: https://github.com/frappe/erpnext/runs/5816574721?check_suite_focus=true#step:12:880 --- .../doctype/quality_procedure/test_quality_procedure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py index daf7a694a35..a26ce2383d1 100644 --- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py @@ -19,7 +19,7 @@ class TestQualityProcedure(unittest.TestCase): ) ).insert() - frappe.form_dict = dict( + frappe.form_dict = frappe._dict( doctype="Quality Procedure", quality_procedure_name="Test Child 1", parent_quality_procedure=procedure.name, From 714fc08150678aebbd6272fc2c156552f36b1eb0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 4 Apr 2022 20:05:10 +0530 Subject: [PATCH 22/49] fix: Do not apply shipping rule for POS transactions (cherry picked from commit c0ebcfb39331caa678d36cc4694490a2363f10a0) # Conflicts: # erpnext/public/js/controllers/taxes_and_totals.js --- erpnext/controllers/taxes_and_totals.py | 5 +++++ erpnext/public/js/controllers/taxes_and_totals.js | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 8183b6e2c99..42cd7de4c99 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -307,6 +307,11 @@ class calculate_taxes_and_totals(object): self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"]) def calculate_shipping_charges(self): + + # Do not apply shipping rule for POS + if self.doc.is_pos: + return + if hasattr(self.doc, "shipping_rule") and self.doc.shipping_rule: shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule) shipping_rule.apply(self.doc) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 047ec814b64..728835795cc 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -272,7 +272,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { }); } +<<<<<<< HEAD calculate_shipping_charges() { +======= + calculate_shipping_charges: function() { + // Do not apply shipping rule for POS + if (this.frm.doc.is_pos) { + return; + } + +>>>>>>> c0ebcfb393 (fix: Do not apply shipping rule for POS transactions) frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) { this.shipping_rule(); From 631545aa3287bde2db15ace490b242c3f3f02c41 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Apr 2022 09:27:40 +0530 Subject: [PATCH 23/49] fix: Use get instead of dot (cherry picked from commit 95298f04000c0299f35cdee7bce0f5f0d8c59525) --- erpnext/controllers/taxes_and_totals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 42cd7de4c99..2144055b343 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -309,7 +309,7 @@ class calculate_taxes_and_totals(object): def calculate_shipping_charges(self): # Do not apply shipping rule for POS - if self.doc.is_pos: + if self.doc.get("is_pos"): return if hasattr(self.doc, "shipping_rule") and self.doc.shipping_rule: From 9bf5f76ac835914bf5124227e564770f93680113 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Apr 2022 17:33:46 +0530 Subject: [PATCH 24/49] fix: Deferred Revenue/Expense Account validation --- erpnext/accounts/deferred_revenue.py | 3 +-- .../doctype/journal_entry/journal_entry.json | 13 +++++++++-- .../process_deferred_accounting.py | 4 ++-- .../sales_invoice/test_sales_invoice.py | 2 +- erpnext/controllers/accounts_controller.py | 22 +++++++++++++++++++ 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 0611f880c5e..a8776fa3448 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -386,7 +386,6 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): doc, credit_account, debit_account, - against, amount, base_amount, end_date, @@ -570,7 +569,6 @@ def book_revenue_via_journal_entry( doc, credit_account, debit_account, - against, amount, base_amount, posting_date, @@ -591,6 +589,7 @@ def book_revenue_via_journal_entry( journal_entry.voucher_type = ( "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense" ) + journal_entry.process_deferred_accounting = deferred_process debit_entry = { "account": credit_account, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 335fd350def..4493c722544 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -3,7 +3,7 @@ "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", - "creation": "2013-03-25 10:53:52", + "creation": "2022-01-25 10:29:58.717206", "doctype": "DocType", "document_type": "Document", "engine": "InnoDB", @@ -13,6 +13,7 @@ "voucher_type", "naming_series", "finance_book", + "process_deferred_accounting", "reversal_of", "tax_withholding_category", "column_break1", @@ -524,13 +525,20 @@ "label": "Reversal Of", "options": "Journal Entry", "read_only": 1 + }, + { + "fieldname": "process_deferred_accounting", + "fieldtype": "Link", + "label": "Process Deferred Accounting", + "options": "Process Deferred Accounting", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 176, "is_submittable": 1, "links": [], - "modified": "2022-01-04 13:39:36.485954", + "modified": "2022-04-06 17:18:46.865259", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", @@ -578,6 +586,7 @@ "search_fields": "voucher_type,posting_date, due_date, cheque_no", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py index 08a7f4110f6..8ec726b36cd 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py @@ -11,7 +11,7 @@ from erpnext.accounts.deferred_revenue import ( convert_deferred_expense_to_expense, convert_deferred_revenue_to_income, ) -from erpnext.accounts.general_ledger import make_reverse_gl_entries +from erpnext.accounts.general_ledger import make_gl_entries class ProcessDeferredAccounting(Document): @@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document): filters={"against_voucher_type": self.doctype, "against_voucher": self.name}, ) - make_reverse_gl_entries(gl_entries=gl_entries) + make_gl_entries(gl_entries=gl_entries, cancel=1) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6c38a7e597a..2309210dd8e 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3104,7 +3104,7 @@ class TestSalesInvoice(unittest.TestCase): acc_settings = frappe.get_single("Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 0 - acc_settings.submit_journal_entriessubmit_journal_entries = 0 + acc_settings.submit_journal_entries = 0 acc_settings.save() frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8a9318e184e..7cbd2bd6c7a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -180,6 +180,7 @@ class AccountsController(TransactionBase): else: self.validate_deferred_start_and_end_date() + self.validate_deferred_income_expense_account() self.set_inter_company_account() if self.doctype == "Purchase Invoice": @@ -208,6 +209,27 @@ class AccountsController(TransactionBase): (self.doctype, self.name), ) + def validate_deferred_income_expense_account(self): + field_map = { + "Sales Invoice": "deferred_revenue_account", + "Purchase Invoice": "deferred_expense_account", + } + + for item in self.get("items"): + if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): + if not item.get(field_map.get(self.doctype)): + default_deferred_account = frappe.db.get_value( + "Company", self.company, "default_" + field_map.get(self.doctype) + ) + if not default_deferred_account: + frappe.throw( + _( + "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master" + ).format(item.idx) + ) + else: + item.set(field_map.get(self.doctype), default_deferred_account) + def validate_deferred_start_and_end_date(self): for d in self.items: if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): From 45fca6bed78ef162cf2969faf75086de94392180 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 7 Apr 2022 13:09:05 +0530 Subject: [PATCH 25/49] feat(india): e-invoicing for intra-state union territory transactions --- .../doctype/gst_account/gst_account.json | 10 +++++++++- erpnext/regional/india/e_invoice/utils.py | 16 ++++++++++++---- erpnext/regional/india/utils.py | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/gst_account/gst_account.json b/erpnext/accounts/doctype/gst_account/gst_account.json index b6ec8844e18..be5124c2d4d 100644 --- a/erpnext/accounts/doctype/gst_account/gst_account.json +++ b/erpnext/accounts/doctype/gst_account/gst_account.json @@ -10,6 +10,7 @@ "sgst_account", "igst_account", "cess_account", + "utgst_account", "is_reverse_charge_account" ], "fields": [ @@ -64,12 +65,18 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Is Reverse Charge Account" + }, + { + "fieldname": "utgst_account", + "fieldtype": "Link", + "label": "UTGST Account", + "options": "Account" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-09 12:30:25.889993", + "modified": "2022-04-07 12:59:14.039768", "modified_by": "Administrator", "module": "Accounts", "name": "GST Account", @@ -78,5 +85,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 8fd9c1c43d2..f3175693128 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -314,10 +314,14 @@ def update_item_taxes(invoice, item): item.cess_rate += item_tax_rate item.cess_amount += abs(item_tax_amount_after_discount) - for tax_type in ["igst", "cgst", "sgst"]: + for tax_type in ["igst", "cgst", "sgst", "utgst"]: if t.account_head in gst_accounts[f"{tax_type}_account"]: item.tax_rate += item_tax_rate - item[f"{tax_type}_amount"] += abs(item_tax_amount) + if tax_type == "utgst": + # utgst taxes are reported same as sgst tax + item["sgst_amount"] += abs(item_tax_amount) + else: + item[f"{tax_type}_amount"] += abs(item_tax_amount) else: # TODO: other charges per item pass @@ -359,11 +363,15 @@ def update_invoice_taxes(invoice, invoice_value_details): # using after discount amt since item also uses after discount amt for cess calc invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) - for tax_type in ["igst", "cgst", "sgst"]: + for tax_type in ["igst", "cgst", "sgst", "utgst"]: if t.account_head in gst_accounts[f"{tax_type}_account"]: + if tax_type == "utgst": + invoice_value_details["total_sgst_amt"] += abs(tax_amount) + else: + invoice_value_details[f"total_{tax_type}_amt"] += abs(tax_amount) - invoice_value_details[f"total_{tax_type}_amt"] += abs(tax_amount) update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows) + else: invoice_value_details.total_other_charges += abs(tax_amount) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 48e17516a5d..765e9eaccdc 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -824,7 +824,7 @@ def get_gst_accounts( gst_settings_accounts = frappe.get_all( "GST Account", filters=filters, - fields=["cgst_account", "sgst_account", "igst_account", "cess_account"], + fields=["cgst_account", "sgst_account", "igst_account", "cess_account", "utgst_account"], ) if not gst_settings_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: From b532ade3839d502a0aab5a67db77654a2077066a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 8 Apr 2022 17:14:10 +0530 Subject: [PATCH 26/49] fix: Download JSON for GSTR-1 report --- erpnext/regional/report/gstr_1/gstr_1.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 9999a6d167b..943bd2c3d20 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -78,8 +78,9 @@ frappe.query_reports["GSTR-1"] = { } }); - report.page.add_inner_button(__("Download as JSON"), function () { + let filters = report.get_values(); + frappe.call({ method: 'erpnext.regional.report.gstr_1.gstr_1.get_json', args: { From 7ff5bc9e3d62ffd0e57600f6383eb865e26ee28f Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 8 Apr 2022 21:33:29 +0530 Subject: [PATCH 27/49] test: Clean up form_dict To avoid failures like https://github.com/frappe/erpnext/runs/5887687369?check_suite_focus=true#step:12:783 --- erpnext/tests/test_exotel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index 68c70ea3444..67bc5bdbb8e 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -8,6 +8,7 @@ from erpnext.hr.doctype.employee.test_employee import make_employee class TestExotel(FrappeAPITestCase): @classmethod def setUpClass(cls): + frappe.form_dict = frappe._dict() cls.CURRENT_DB_CONNECTION = frappe.db cls.test_employee_name = make_employee( user="test_employee_exotel@company.com", cell_number="9999999999" @@ -37,7 +38,6 @@ class TestExotel(FrappeAPITestCase): api_method = "handle_missed_call" self.emulate_api_call_from_exotel(api_method, call_disconnected_data) call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid) - self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom) self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber) self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) From e30cc8422c1621070934e433fe3e13d407325081 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sat, 9 Apr 2022 19:53:40 +0530 Subject: [PATCH 28/49] fix: Resolve conflicts --- erpnext/public/js/controllers/taxes_and_totals.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 728835795cc..7a48fbdbd96 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -272,16 +272,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { }); } -<<<<<<< HEAD calculate_shipping_charges() { -======= - calculate_shipping_charges: function() { // Do not apply shipping rule for POS if (this.frm.doc.is_pos) { return; } ->>>>>>> c0ebcfb393 (fix: Do not apply shipping rule for POS transactions) frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) { this.shipping_rule(); From 03c631d7238a0e058235a3e4057d88451cd97ee6 Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Mon, 11 Apr 2022 07:19:18 +0200 Subject: [PATCH 29/49] fix: update translation (#30654) --- erpnext/translations/fr.csv | 67 +++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index db454def738..3295136047e 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -951,14 +951,14 @@ End time cannot be before start time,L'heure de fin ne peut pas être avant l'he Ends On date cannot be before Next Contact Date.,La date de fin ne peut pas être avant la prochaine date de contact, Energy,Énergie, Engineer,Ingénieur, -Enough Parts to Build,Pièces Suffisantes pour Construire, +Enough Parts to Build,Pièces Suffisantes pour Construire Enroll,Inscrire, Enrolling student,Inscrire un étudiant, Enrolling students,Inscription des étudiants, Enter depreciation details,Veuillez entrer les détails de l'amortissement, -Enter the Bank Guarantee Number before submittting.,Entrez le numéro de garantie bancaire avant de soumettre., -Enter the name of the Beneficiary before submittting.,Entrez le nom du bénéficiaire avant de soumettre., -Enter the name of the bank or lending institution before submittting.,Entrez le nom de la banque ou de l'institution de prêt avant de soumettre., +Enter the Bank Guarantee Number before submittting.,Entrez le numéro de garantie bancaire avant de valider. +Enter the name of the Beneficiary before submittting.,Entrez le nom du bénéficiaire avant de valider. +Enter the name of the bank or lending institution before submittting.,Entrez le nom de la banque ou de l'institution de prêt avant de valider., Enter value betweeen {0} and {1},Entrez une valeur entre {0} et {1}, Entertainment & Leisure,Divertissement et Loisir, Entertainment Expenses,Charges de Représentation, @@ -1068,7 +1068,7 @@ For Employee,Employé, For Quantity (Manufactured Qty) is mandatory,Pour Quantité (Qté Produite) est obligatoire, For Supplier,Pour Fournisseur, For Warehouse,Pour l’Entrepôt, -For Warehouse is required before Submit,Pour l’Entrepôt est requis avant de Soumettre, +For Warehouse is required before Submit,Pour l’Entrepôt est requis avant de Valider, "For an item {0}, quantity must be negative number","Pour l'article {0}, la quantité doit être un nombre négatif", "For an item {0}, quantity must be positive number","Pour un article {0}, la quantité doit être un nombre positif", "For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry","Pour la carte de travail {0}, vous pouvez uniquement saisir une entrée de stock de type "Transfert d'article pour fabrication".", @@ -1693,7 +1693,7 @@ No Items with Bill of Materials to Manufacture,Aucun Article avec une Liste de M No Items with Bill of Materials.,Aucun article avec nomenclature., No Permission,Aucune autorisation, No Remarks,Aucune Remarque, -No Result to submit,Aucun résultat à soumettre, +No Result to submit,Aucun résultat à valider, No Salary Structure assigned for Employee {0} on given date {1},Aucune structure de salaire attribuée à l'employé {0} à la date donnée {1}, No Staffing Plans found for this Designation,Aucun plan de dotation trouvé pour cette désignation, No Student Groups created.,Aucun Groupe d'Étudiants créé., @@ -2847,12 +2847,12 @@ Sub Type,Sous type, Sub-contracting,Sous-traitant, Subcontract,Sous-traiter, Subject,Sujet, -Submit,Soumettre, -Submit Proof,Soumettre une preuve, -Submit Salary Slip,Soumettre la Fiche de Paie, -Submit this Work Order for further processing.,Soumettre cet ordre de travail pour continuer son traitement., -Submit this to create the Employee record,Soumettre pour créer la fiche employé, -Submitting Salary Slips...,Soumission des bulletins de salaire ..., +Submit,Valider, +Submit Proof,Valider une preuve, +Submit Salary Slip,Valider la Fiche de Paie, +Submit this Work Order for further processing.,Valider cet ordre de travail pour continuer son traitement., +Submit this to create the Employee record,Valider pour créer la fiche employé, +Submitting Salary Slips...,Validation des bulletins de salaire ..., Subscription,Abonnement, Subscription Management,Gestion des abonnements, Subscriptions,Abonnements, @@ -2954,7 +2954,7 @@ The Term End Date cannot be earlier than the Term Start Date. Please correct the The Term End Date cannot be later than the Year End Date of the Academic Year to which the term is linked (Academic Year {}). Please correct the dates and try again.,La Date de Fin de Terme ne peut pas être postérieure à la Date de Fin de l'Année Académique à laquelle le terme est lié (Année Académique {}). Veuillez corriger les dates et essayer à nouveau., The Term Start Date cannot be earlier than the Year Start Date of the Academic Year to which the term is linked (Academic Year {}). Please correct the dates and try again.,La Date de Début de Terme ne peut pas être antérieure à la Date de Début de l'Année Académique à laquelle le terme est lié (Année Académique {}). Veuillez corriger les dates et essayer à nouveau., The Year End Date cannot be earlier than the Year Start Date. Please correct the dates and try again.,La Date de Fin d'Année ne peut pas être antérieure à la Date de Début d’Année. Veuillez corriger les dates et essayer à nouveau., -The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document.,Le montant {0} défini dans cette requête de paiement est différent du montant calculé de tous les plans de paiement: {1}.\nVeuillez vérifier que c'est correct avant de soumettre le document., +The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document.,Le montant {0} défini dans cette requête de paiement est différent du montant calculé de tous les plans de paiement: {1}.\nVeuillez vérifier que c'est correct avant de valider le document., The day(s) on which you are applying for leave are holidays. You need not apply for leave.,Le(s) jour(s) pour le(s)quel(s) vous demandez un congé sont des jour(s) férié(s). Vous n’avez pas besoin d’effectuer de demande., The field From Shareholder cannot be blank,Le champ 'De l'actionnaire' ne peut pas être vide, The field To Shareholder cannot be blank,Le champ 'A l'actionnaire' ne peut pas être vide, @@ -3011,7 +3011,7 @@ This is based on transactions against this Healthcare Practitioner.,Ce graphique This is based on transactions against this Patient. See timeline below for details,Ceci est basé sur les transactions de ce patient. Voir la chronologie ci-dessous pour plus de détails, This is based on transactions against this Sales Person. See timeline below for details,Ceci est basé sur les transactions contre ce vendeur. Voir la chronologie ci-dessous pour plus de détails, This is based on transactions against this Supplier. See timeline below for details,Basé sur les transactions avec ce fournisseur. Voir la chronologie ci-dessous pour plus de détails, -This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?,Cela permettra de soumettre des bulletins de salaire et de créer une écriture de journal d'accumulation. Voulez-vous poursuivre?, +This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?,Cela permettra de valider des bulletins de salaire et de créer une écriture de journal d'accumulation. Voulez-vous poursuivre?, This {0} conflicts with {1} for {2} {3},Ce {0} est en conflit avec {1} pour {2} {3}, Time Sheet for manufacturing.,Feuille de Temps pour la production., Time Tracking,Suivi du temps, @@ -3312,7 +3312,7 @@ Work Order {0} must be cancelled before cancelling this Sales Order,L'ordre de t Work Order {0} must be submitted,L'ordre de travail {0} doit être soumis, Work Orders Created: {0},Ordres de travail créés: {0}, Work Summary for {0},Résumé de travail de {0}, -Work-in-Progress Warehouse is required before Submit,L'entrepôt des Travaux en Cours est nécessaire avant de Soumettre, +Work-in-Progress Warehouse is required before Submit,L'entrepôt des Travaux en Cours est nécessaire avant de Valider, Workflow,Flux de Travail, Working,Travail en cours, Working Hours,Heures de travail, @@ -3331,7 +3331,7 @@ You can only have Plans with the same billing cycle in a Subscription,Vous ne po You can only redeem max {0} points in this order.,Vous pouvez uniquement échanger un maximum de {0} points dans cet commande., You can only renew if your membership expires within 30 days,Vous ne pouvez renouveler que si votre abonnement expire dans les 30 jours, You can only select a maximum of one option from the list of check boxes.,Vous pouvez sélectionner au maximum une option dans la liste des cases à cocher., -You can only submit Leave Encashment for a valid encashment amount,Vous pouvez uniquement soumettre un encaissement de congé pour un montant d'encaissement valide, +You can only submit Leave Encashment for a valid encashment amount,Vous pouvez uniquement valider un encaissement de congé pour un montant d'encaissement valide, You can't redeem Loyalty Points having more value than the Grand Total.,Vous ne pouvez pas échanger des points de fidélité ayant plus de valeur que le total général., You cannot credit and debit same account at the same time,Vous ne pouvez pas créditer et débiter le même compte simultanément, You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings,Vous ne pouvez pas supprimer l'exercice fiscal {0}. L'exercice fiscal {0} est défini par défaut dans les Paramètres Globaux, @@ -3684,8 +3684,8 @@ Create Quality Inspection for Item {0},Créer un contrôle qualité pour l'artic Creating Accounts...,Création de comptes ..., Creating bank entries...,Création d'entrées bancaires ..., Credit limit is already defined for the Company {0},La limite de crédit est déjà définie pour la société {0}., -Ctrl + Enter to submit,Ctrl + Entrée pour soumettre, -Ctrl+Enter to submit,Ctrl + Entrée pour soumettre, +Ctrl + Enter to submit,Ctrl + Entrée pour valider, +Ctrl+Enter to submit,Ctrl + Entrée pour valider, Currency,Devise, Current Status,Statut Actuel, Customer PO,Bon de commande client, @@ -3709,7 +3709,7 @@ Dimension Filter,Filtre de dimension, Disabled,Desactivé, Disbursement and Repayment,Décaissement et remboursement, Distance cannot be greater than 4000 kms,La distance ne peut pas dépasser 4000 km, -Do you want to submit the material request,Voulez-vous soumettre la demande de matériel, +Do you want to submit the material request,Voulez-vous valider la demande de matériel, Doctype,Doctype, Document {0} successfully uncleared,Document {0} non effacé avec succès, Download Template,Télécharger le Modèle, @@ -4309,7 +4309,7 @@ Requested,Demandé, Partially Paid,Partiellement payé, Invalid Account Currency,Devise de compte non valide, "Row {0}: The item {1}, quantity must be positive number","Ligne {0}: l'article {1}, la quantité doit être un nombre positif", -"Please set {0} for Batched Item {1}, which is used to set {2} on Submit.","Veuillez définir {0} pour l'article par lots {1}, qui est utilisé pour définir {2} sur Soumettre.", +"Please set {0} for Batched Item {1}, which is used to set {2} on Submit.","Veuillez définir {0} pour l'article par lots {1}, qui est utilisé pour définir {2} sur Valider.", Expiry Date Mandatory,Date d'expiration obligatoire, Variant Item,Élément de variante, BOM 1 {0} and BOM 2 {1} should not be same,La nomenclature 1 {0} et la nomenclature 2 {1} ne doivent pas être identiques, @@ -4589,7 +4589,7 @@ Bank Transaction Entries,Ecritures de transactions bancaires, New Transactions,Nouvelles transactions, Match Transaction to Invoices,Faire correspondre la transaction aux factures, Create New Payment/Journal Entry,Créer un nouveau paiement / écriture de journal, -Submit/Reconcile Payments,Soumettre / rapprocher les paiements, +Submit/Reconcile Payments,Valider / rapprocher les paiements, Matching Invoices,Factures correspondantes, Payment Invoice Items,Articles de la facture de paiement, Reconciled Transactions,Transactions rapprochées, @@ -6208,7 +6208,7 @@ Collect Fee for Patient Registration,Collecter les honoraires pour l'inscription Checking this will create new Patients with a Disabled status by default and will only be enabled after invoicing the Registration Fee.,Cochez cette case pour créer de nouveaux patients avec un statut Désactivé par défaut et ne seront activés qu'après facturation des frais d'inscription., Registration Fee,Frais d'Inscription, Automate Appointment Invoicing,Automatiser la facturation des rendez-vous, -Manage Appointment Invoice submit and cancel automatically for Patient Encounter,Gérer les factures de rendez-vous soumettre et annuler automatiquement pour la consultation des patients, +Manage Appointment Invoice submit and cancel automatically for Patient Encounter,Gérer les factures de rendez-vous valider et annuler automatiquement pour la consultation des patients, Enable Free Follow-ups,Activer les suivis gratuits, Number of Patient Encounters in Valid Days,Nombre de rencontres de patients en jours valides, The number of free follow ups (Patient Encounters in valid days) allowed,Le nombre de suivis gratuits (rencontres de patients en jours valides) autorisés, @@ -8679,7 +8679,7 @@ Book Deferred Entries Based On,Enregistrer les entrées différées en fonction Days,Journées, Months,Mois, Book Deferred Entries Via Journal Entry,Enregistrer les écritures différées via l'écriture au journal, -Submit Journal Entries,Soumettre les entrées de journal, +Submit Journal Entries,Valider les entrées de journal, If this is unchecked Journal Entries will be saved in a Draft state and will have to be submitted manually,"Si cette case n'est pas cochée, les entrées de journal seront enregistrées dans un état Brouillon et devront être soumises manuellement", Enable Distributed Cost Center,Activer le centre de coûts distribués, Distributed Cost Center,Centre de coûts distribués, @@ -9065,7 +9065,7 @@ Rented To Date,Loué à ce jour, Monthly Eligible Amount,Montant mensuel admissible, Total Eligible HRA Exemption,Exemption HRA totale éligible, Validating Employee Attendance...,Validation de la présence des employés ..., -Submitting Salary Slips and creating Journal Entry...,Soumettre des fiches de salaire et créer une écriture au journal ..., +Submitting Salary Slips and creating Journal Entry...,Validation des fiches de salaire et créer une écriture au journal ..., Calculate Payroll Working Days Based On,Calculer les jours ouvrables de paie en fonction de, Consider Unmarked Attendance As,Considérez la participation non marquée comme, Fraction of Daily Salary for Half Day,Fraction du salaire journalier pour une demi-journée, @@ -9166,8 +9166,8 @@ Enter customer's phone number,Entrez le numéro de téléphone du client, Customer contact updated successfully.,Contact client mis à jour avec succès., Item will be removed since no serial / batch no selected.,L'article sera supprimé car aucun numéro de série / lot sélectionné., Discount (%),Remise (%), -You cannot submit the order without payment.,Vous ne pouvez pas soumettre la commande sans paiement., -You cannot submit empty order.,Vous ne pouvez pas soumettre de commande vide., +You cannot submit the order without payment.,Vous ne pouvez pas valider la commande sans paiement., +You cannot submit empty order.,Vous ne pouvez pas valider de commande vide., To Be Paid,Être payé, Create POS Opening Entry,Créer une entrée d'ouverture de PDV, Please add Mode of payments and opening balance details.,Veuillez ajouter le mode de paiement et les détails du solde d'ouverture., @@ -9305,7 +9305,7 @@ Courses updated,Cours mis à jour, {0} {1} has been added to all the selected topics successfully.,{0} {1} a bien été ajouté à tous les sujets sélectionnés., Topics updated,Sujets mis à jour, Academic Term and Program,Terme académique et programme, -Please remove this item and try to submit again or update the posting time.,Veuillez supprimer cet élément et réessayer de le soumettre ou mettre à jour l'heure de publication., +Please remove this item and try to submit again or update the posting time.,Veuillez supprimer cet élément et réessayer de le valider ou mettre à jour l'heure de publication., Failed to Authenticate the API key.,Échec de l'authentification de la clé API., Invalid Credentials,Les informations d'identification invalides, URL can only be a string,L'URL ne peut être qu'une chaîne, @@ -9416,7 +9416,7 @@ Import Italian Supplier Invoice.,Importer la facture du fournisseur italien., "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.",Le taux de valorisation de l'article {0} est requis pour effectuer des écritures comptables pour {1} {2}., Here are the options to proceed:,Voici les options pour continuer:, "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table.","Si l'article est traité comme un article à taux de valorisation nul dans cette entrée, veuillez activer "Autoriser le taux de valorisation nul" dans le {0} tableau des articles.", -"If not, you can Cancel / Submit this entry ","Sinon, vous pouvez annuler / soumettre cette entrée", +"If not, you can Cancel / Submit this entry ","Sinon, vous pouvez annuler / valider cette entrée", performing either one below:,effectuer l'un ou l'autre ci-dessous:, Create an incoming stock transaction for the Item.,Créez une transaction de stock entrante pour l'article., Mention Valuation Rate in the Item master.,Mentionnez le taux de valorisation dans la fiche article., @@ -9573,7 +9573,7 @@ Accounting entries are frozen up to this date. Nobody can create or modify entri Role Allowed to Set Frozen Accounts and Edit Frozen Entries,Rôle autorisé à définir des comptes gelés et à modifier les entrées gelées, Address used to determine Tax Category in transactions,Adresse utilisée pour déterminer la catégorie de taxe dans les transactions, "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ","Le pourcentage que vous êtes autorisé à facturer davantage par rapport au montant commandé. Par exemple, si la valeur de la commande est de 100 USD pour un article et que la tolérance est définie sur 10%, vous êtes autorisé à facturer jusqu'à 110 USD.", -This role is allowed to submit transactions that exceed credit limits,Ce rôle est autorisé à soumettre des transactions qui dépassent les limites de crédit, +This role is allowed to submit transactions that exceed credit limits,Ce rôle est autorisé à valider des transactions qui dépassent les limites de crédit, "If ""Months"" is selected, a fixed amount will be booked as deferred revenue or expense for each month irrespective of the number of days in a month. It will be prorated if deferred revenue or expense is not booked for an entire month","Si «Mois» est sélectionné, un montant fixe sera comptabilisé en tant que revenus ou dépenses différés pour chaque mois, quel que soit le nombre de jours dans un mois. Il sera calculé au prorata si les revenus ou les dépenses différés ne sont pas comptabilisés pour un mois entier", "If this is unchecked, direct GL entries will be created to book deferred revenue or expense","Si cette case n'est pas cochée, des entrées GL directes seront créées pour enregistrer les revenus ou les dépenses différés", Show Inclusive Tax in Print,Afficher la taxe incluse en version imprimée, @@ -9744,7 +9744,7 @@ Print Receipt,Imprimer le reçu, Edit Receipt,Modifier le reçu, Focus on search input,Focus sur l'entrée de recherche, Focus on Item Group filter,Focus sur le filtre de groupe d'articles, -Checkout Order / Submit Order / New Order,Commander la commande / Soumettre la commande / Nouvelle commande, +Checkout Order / Submit Order / New Order,Commander la commande / Valider la commande / Nouvelle commande, Add Order Discount,Ajouter une remise de commande, Item Code: {0} is not available under warehouse {1}.,Code d'article: {0} n'est pas disponible dans l'entrepôt {1}., Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.,Numéros de série non disponibles pour l'article {0} sous l'entrepôt {1}. Veuillez essayer de changer d’entrepôt., @@ -9787,11 +9787,11 @@ because expense is booked against this account in Purchase Receipt {},car les d as no Purchase Receipt is created against Item {}. ,car aucun reçu d'achat n'est créé pour l'article {}., This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice,Ceci est fait pour gérer la comptabilité des cas où le reçu d'achat est créé après la facture d'achat, Purchase Order Required for item {},Bon de commande requis pour l'article {}, -To submit the invoice without purchase order please set {} ,"Pour soumettre la facture sans bon de commande, veuillez définir {}", +To submit the invoice without purchase order please set {} ,"Pour valider la facture sans bon de commande, veuillez définir {}", as {} in {},un péché {}, Mandatory Purchase Order,Bon de commande obligatoire, Purchase Receipt Required for item {},Reçu d'achat requis pour l'article {}, -To submit the invoice without purchase receipt please set {} ,"Pour soumettre la facture sans reçu d'achat, veuillez définir {}", +To submit the invoice without purchase receipt please set {} ,"Pour valider la facture sans reçu d'achat, veuillez définir {}", Mandatory Purchase Receipt,Reçu d'achat obligatoire, POS Profile {} does not belongs to company {},Le profil PDV {} n'appartient pas à l'entreprise {}, User {} is disabled. Please select valid user/cashier,L'utilisateur {} est désactivé. Veuillez sélectionner un utilisateur / caissier valide, @@ -9864,9 +9864,10 @@ Control Historical Stock Transactions,Controle de l'historique des stransact No stock transactions can be created or modified before this date.,Aucune transaction ne peux être créée ou modifié avant cette date. Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée -"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","LEs utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire" +"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","Les utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire" Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries +"The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités" From 086d31b59f2a960b463c88ea1c32fac71958b142 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 26 Mar 2022 13:06:20 +0530 Subject: [PATCH 30/49] fix: dont validate currency exchange in setup Redues ~4-5 seconds of time and chances of setup failure. --- .../currency_exchange_settings.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py index edea37dcfd9..d618c5ca117 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -11,6 +11,8 @@ from frappe.utils import nowdate class CurrencyExchangeSettings(Document): def validate(self): self.set_parameters_and_result() + if frappe.flags.in_test or frappe.flags.in_install or frappe.flags.in_setup_wizard: + return response, value = self.validate_parameters() self.validate_result(response, value) @@ -35,9 +37,6 @@ class CurrencyExchangeSettings(Document): self.append("req_params", {"key": "symbols", "value": "{to_currency}"}) def validate_parameters(self): - if frappe.flags.in_test: - return None, None - params = {} for row in self.req_params: params[row.key] = row.value.format( @@ -59,9 +58,6 @@ class CurrencyExchangeSettings(Document): return response, value def validate_result(self, response, value): - if frappe.flags.in_test: - return - try: for key in self.result_key: value = value[ From c3e1f0e3692e87e8f2c9385f4a24271dfab47138 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 27 Mar 2022 11:52:31 +0530 Subject: [PATCH 31/49] refactor: ignore mandatory fields during setup --- .../doctype/selling_settings/test_selling_settings.py | 9 +++++++-- erpnext/setup/install.py | 9 ++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/selling_settings/test_selling_settings.py b/erpnext/selling/doctype/selling_settings/test_selling_settings.py index fc6754a7c59..7290e685b2f 100644 --- a/erpnext/selling/doctype/selling_settings/test_selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/test_selling_settings.py @@ -1,9 +1,14 @@ # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe + class TestSellingSettings(unittest.TestCase): - pass + def test_defaults_populated(self): + # Setup default values are not populated on migrate, this test checks + # if setup was completed correctly + default = frappe.db.get_single_value("Selling Settings", "maintain_same_rate_action") + self.assertEqual("Stop", default) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 2b055d2dd36..8dae23db774 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -56,12 +56,11 @@ def set_single_defaults(): ) if default_values: try: - b = frappe.get_doc(dt, dt) + doc = frappe.get_doc(dt, dt) for fieldname, value in default_values: - b.set(fieldname, value) - b.save() - except frappe.MandatoryError: - pass + doc.set(fieldname, value) + doc.flags.ignore_mandatory = True + doc.save() except frappe.ValidationError: pass From dfff4beaf47da9a044f0a293a7fc50f8cda3c51e Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 11 Apr 2022 14:10:57 +0530 Subject: [PATCH 32/49] fix: Handle multiple item transfer in separate SEs against WO - Check for pending qty in child items to show/hide "Start" button - If no qty needed to transfer (FG qty is fulfilled), but RM qty pending: map pending in SE with For Quantity = 0 --- erpnext/manufacturing/doctype/work_order/work_order.js | 6 ++++-- erpnext/stock/doctype/stock_entry/stock_entry.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 6433a992830..2751778fe40 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -540,8 +540,10 @@ erpnext.work_order = { || frm.doc.transfer_material_against == 'Job Card') ? 0 : 1; if (show_start_btn) { - if ((flt(doc.material_transferred_for_manufacturing) < flt(doc.qty)) - && frm.doc.status != 'Stopped') { + let pending_to_transfer = frm.doc.required_items.some( + item => flt(item.transferred_qty) < flt(item.required_qty) + ) + if (pending_to_transfer && frm.doc.status != 'Stopped') { frm.has_start_btn = true; frm.add_custom_button(__('Create Pick List'), function() { erpnext.work_order.create_pick_list(frm); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 1e624714d05..c4aa8a4711b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1803,7 +1803,9 @@ class StockEntry(StockController): or (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture") or allow_overproduction ): - item_dict[item]["qty"] = desire_to_transfer + # "No need for transfer but qty still pending to transfer" case can occur + # when transferring multiple RM in different Stock Entries + item_dict[item]["qty"] = desire_to_transfer if (desire_to_transfer > 0) else pending_to_issue elif pending_to_issue > 0: item_dict[item]["qty"] = pending_to_issue else: From 553178bfe72e7031a7fe775fc5d2136c9a5aefcf Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 11 Apr 2022 15:29:20 +0530 Subject: [PATCH 33/49] test: Add test --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 2309210dd8e..7781fe3391a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2240,6 +2240,14 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, "2019-01-30") + def test_deferred_revenue_missing_account(self): + si = create_sales_invoice(posting_date="2019-01-10", do_not_submit=True) + si.items[0].enable_deferred_revenue = 1 + si.items[0].service_start_date = "2019-01-10" + si.items[0].service_end_date = "2019-03-15" + + self.assertRaises(frappe.ValidationError, si.save) + def test_fixed_deferred_revenue(self): deferred_account = create_account( account_name="Deferred Revenue", From be2e5ce966bb206777c35398ae6fb99f1dcd7e79 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 11 Apr 2022 15:32:36 +0530 Subject: [PATCH 34/49] style: Missing Semicolon --- erpnext/manufacturing/doctype/work_order/work_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 2751778fe40..20f15039efe 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -542,7 +542,7 @@ erpnext.work_order = { if (show_start_btn) { let pending_to_transfer = frm.doc.required_items.some( item => flt(item.transferred_qty) < flt(item.required_qty) - ) + ); if (pending_to_transfer && frm.doc.status != 'Stopped') { frm.has_start_btn = true; frm.add_custom_button(__('Create Pick List'), function() { From 5aa60bb65142a9dbd2fbbec5c26150cd559275e6 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 11 Apr 2022 16:44:55 +0530 Subject: [PATCH 35/49] test: Multiple RM transfer in separate Stock Entries - Added test and acceptance of 0 as For Quantity in test helper --- .../doctype/work_order/test_work_order.py | 50 +++++++++++++++++++ .../doctype/work_order/work_order.py | 6 ++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 8934f9c4e09..2aba48231be 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1144,6 +1144,56 @@ class TestWorkOrder(FrappeTestCase): for index, row in enumerate(ste_manu.get("items"), start=1): self.assertEqual(index, row.idx) + @change_settings( + "Manufacturing Settings", + {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}, + ) + def test_work_order_multiple_material_transfer(self): + """ + Test transferring multiple RMs in separate Stock Entries. + """ + work_order = make_wo_order_test_record(planned_start_date=now(), qty=1) + test_stock_entry.make_stock_entry( # stock up RM + item_code="_Test Item", + target="_Test Warehouse - _TC", + qty=1, + basic_rate=5000.0, + ) + test_stock_entry.make_stock_entry( # stock up RM + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=2, + basic_rate=1000.0, + ) + + transfer_entry = frappe.get_doc( + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1) + ) + del transfer_entry.get("items")[0] # transfer only one RM + transfer_entry.submit() + + # WO's "Material Transferred for Mfg" shows all is transferred, one RM is pending + work_order.reload() + self.assertEqual(work_order.material_transferred_for_manufacturing, 1) + self.assertEqual(work_order.required_items[0].transferred_qty, 0) + self.assertEqual(work_order.required_items[1].transferred_qty, 2) + + final_transfer_entry = frappe.get_doc( # transfer last RM with For Quantity = 0 + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 0) + ) + final_transfer_entry.save() + + self.assertEqual(final_transfer_entry.fg_completed_qty, 0.0) + self.assertEqual(final_transfer_entry.items[0].qty, 1) + + final_transfer_entry.submit() + work_order.reload() + + # WO's "Material Transferred for Mfg" shows all is transferred, no RM is pending + self.assertEqual(work_order.material_transferred_for_manufacturing, 1) + self.assertEqual(work_order.required_items[0].transferred_qty, 1) + self.assertEqual(work_order.required_items[1].transferred_qty, 2) + def update_job_card(job_card, jc_qty=None): employee = frappe.db.get_value("Employee", {"status": "Active"}, "name") diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 2ee848c356a..2802310250b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1186,7 +1186,11 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.from_bom = 1 stock_entry.bom_no = work_order.bom_no stock_entry.use_multi_level_bom = work_order.use_multi_level_bom - stock_entry.fg_completed_qty = qty or (flt(work_order.qty) - flt(work_order.produced_qty)) + # accept 0 qty as well + stock_entry.fg_completed_qty = ( + qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty)) + ) + if work_order.bom_no: stock_entry.inspection_required = frappe.db.get_value( "BOM", work_order.bom_no, "inspection_required" From 60fb71bd2a6bfb0a223b233759c8aa8cbda7a1bb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 12 Apr 2022 11:14:02 +0530 Subject: [PATCH 36/49] fix: ignore item-less maintenance visit for sr no (#30684) --- .../maintenance/doctype/maintenance_visit/maintenance_visit.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index 72686e7403f..e2f6cb3a6cc 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -12,6 +12,9 @@ frappe.ui.form.on('Maintenance Visit', { // filters for serial no based on item code if (frm.doc.maintenance_type === "Scheduled") { let item_code = frm.doc.purposes[0].item_code; + if (!item_code) { + return; + } frappe.call({ method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule", args: { From 993c6c0de9c1e3cfc3c4673e3bc16542204022be Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 12 Apr 2022 12:24:47 +0530 Subject: [PATCH 37/49] 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 eda36868b9f..233b476a14b 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 7877827ac79..19fedb3c38f 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 38/49] 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 60b32b84b05..6af1342945e 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 39/49] 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 897151a97b7..445378300bb 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 40/49] 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 035290d8f19..5252798ba57 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 9a23c071061..04c080cc72e 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 a98cd10e320..2268e0f7afa 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 41/49] 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 6af1342945e..9ca05b927f3 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 42/49] 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 6e5ffed7797..63b6bb73e88 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 fc3e68ac677..00000000000 --- 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 cc358135acd..00000000000 --- 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 00000000000..a5de91f9c2b --- /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 e69de29bb2d..00000000000 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 3c365494c49..00000000000 --- 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 f60de4c8af4..00000000000 --- 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 686a93e529d..00000000000 --- 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 ba70eb472fe..00000000000 --- 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 e69de29bb2d..00000000000 diff --git a/erpnext/regional/germany/setup.py b/erpnext/regional/germany/setup.py deleted file mode 100644 index b8e66c3ece3..00000000000 --- 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 e69de29bb2d..00000000000 diff --git a/erpnext/regional/germany/utils/datev/__init__.py b/erpnext/regional/germany/utils/datev/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 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 9524481c117..00000000000 --- 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 d4e9c279b81..00000000000 --- 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 e69de29bb2d..00000000000 diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js deleted file mode 100644 index 03c729e6df4..00000000000 --- 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 94e3960eade..00000000000 --- 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 2d888a8762b..00000000000 --- 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 0df8c0607d5..00000000000 --- 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 43/49] 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 657054fb240..85ff515772d 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 44/49] 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 8cc582634a9..cdb68499ffd 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 45/49] test: Do not overwrite frappe.form_dict to retain proxy reference Set frappe.local.form_dict instead --- .../doctype/request_for_quotation/test_request_for_quotation.py | 1 - .../doctype/quality_procedure/test_quality_procedure.py | 2 +- erpnext/tests/test_exotel.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index dcdba095fbf..064b806e953 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -65,7 +65,6 @@ class TestRequestforQuotation(FrappeTestCase): ) sq.submit() - frappe.form_dict = frappe.local("form_dict") frappe.form_dict.name = rfq.name self.assertEqual(check_supplier_has_docname_access(supplier_wt_appos[0].get("supplier")), True) diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py index a26ce2383d1..04e82112142 100644 --- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py @@ -19,7 +19,7 @@ class TestQualityProcedure(unittest.TestCase): ) ).insert() - frappe.form_dict = frappe._dict( + frappe.local.form_dict = frappe._dict( doctype="Quality Procedure", quality_procedure_name="Test Child 1", parent_quality_procedure=procedure.name, diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py index 67bc5bdbb8e..bb916a8092b 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -8,7 +8,6 @@ from erpnext.hr.doctype.employee.test_employee import make_employee class TestExotel(FrappeAPITestCase): @classmethod def setUpClass(cls): - frappe.form_dict = frappe._dict() cls.CURRENT_DB_CONNECTION = frappe.db cls.test_employee_name = make_employee( user="test_employee_exotel@company.com", cell_number="9999999999" From ff41b8da4e3ae40b07847193c0ad40848feb9ced Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 13 Apr 2022 20:12:08 +0530 Subject: [PATCH 46/49] 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 2092ec284b5..7725e71f19c 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 bb916a8092b..76bbb3e05ad 100644 --- a/erpnext/tests/test_exotel.py +++ b/erpnext/tests/test_exotel.py @@ -12,6 +12,7 @@ class TestExotel(FrappeAPITestCase): cls.test_employee_name = make_employee( user="test_employee_exotel@company.com", cell_number="9999999999" ) + frappe.db.set_value("Exotel Settings", "Exotel Settings", "enabled", 1) phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}] create_contact(name="Test Contact", salutation="Mr", phones=phones) frappe.db.commit() From 79fbc2c504f94d898e118a9af0f37725e79c4323 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 13 Apr 2022 20:42:25 +0530 Subject: [PATCH 47/49] 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 04e82112142..2a47a01a58b 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 f3faba4f8d5..ce3bd56d55a 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 48/49] 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 cd749e8a018..c0d416c1c29 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 49/49] 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 2a47a01a58b..04e82112142 100644 --- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py @@ -19,7 +19,6 @@ class TestQualityProcedure(unittest.TestCase): ) ).insert() - frappe.local.form_dict = frappe._dict( doctype="Quality Procedure", quality_procedure_name="Test Child 1",