From 37b8715096df6a7e645fedd40bc42e869857fcd7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 9 Sep 2024 12:31:32 +0530 Subject: [PATCH 1/5] feat: utility report to identify invalid ledger entries (cherry picked from commit 832c4aaf82e6556a83429a3757efb3449b53de2a) --- .../report/invalid_ledger_entries/__init__.py | 0 .../invalid_ledger_entries.js | 13 +++++ .../invalid_ledger_entries.json | 23 +++++++++ .../invalid_ledger_entries.py | 48 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 erpnext/accounts/report/invalid_ledger_entries/__init__.py create mode 100644 erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js create mode 100644 erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json create mode 100644 erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py diff --git a/erpnext/accounts/report/invalid_ledger_entries/__init__.py b/erpnext/accounts/report/invalid_ledger_entries/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js new file mode 100644 index 00000000000..548a6f7d951 --- /dev/null +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js @@ -0,0 +1,13 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Invalid Ledger Entries"] = { + filters: [ + // { + // "fieldname": "my_filter", + // "label": __("My Filter"), + // "fieldtype": "Data", + // "reqd": 1, + // }, + ], +}; diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json new file mode 100644 index 00000000000..00dbbfc5056 --- /dev/null +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json @@ -0,0 +1,23 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2024-09-09 12:31:25.295976", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2024-09-09 12:31:25.295976", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Invalid Ledger Entries", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "Invalid Ledger Entries", + "report_type": "Script Report", + "roles": [], + "timeout": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py new file mode 100644 index 00000000000..4f4b1835227 --- /dev/null +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py @@ -0,0 +1,48 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe import _ + + +def execute(filters: dict | None = None): + """Return columns and data for the report. + + This is the main entry point for the report. It accepts the filters as a + dictionary and should return columns and data. It is called by the framework + every time the report is refreshed or a filter is updated. + """ + columns = get_columns() + data = get_data() + + return columns, data + + +def get_columns() -> list[dict]: + """Return columns for the report. + + One field definition per column, just like a DocType field definition. + """ + return [ + { + "label": _("Column 1"), + "fieldname": "column_1", + "fieldtype": "Data", + }, + { + "label": _("Column 2"), + "fieldname": "column_2", + "fieldtype": "Int", + }, + ] + + +def get_data() -> list[list]: + """Return data for the report. + + The report data is a list of rows, with each row being a list of cell values. + """ + return [ + ["Row 1", 1], + ["Row 2", 2], + ] From 3574d119465661ee4bde3552b43038359aa5524e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 9 Sep 2024 13:55:10 +0530 Subject: [PATCH 2/5] refactor: standard filters (cherry picked from commit dccbc1f432a83f07ef46582ac36df8fd776d4a4d) --- .../invalid_ledger_entries.js | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js index 548a6f7d951..ffaaf5d7cbf 100644 --- a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js @@ -1,13 +1,52 @@ // Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +function get_filters() { + let filters = [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + { + fieldname: "from_date", + label: __("Start Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + fieldname: "to_date", + label: __("End Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.get_today(), + }, + { + fieldname: "account", + label: __("Account"), + fieldtype: "MultiSelectList", + options: "Account", + get_data: function (txt) { + return frappe.db.get_link_options("Account", txt, { + company: frappe.query_report.get_filter_value("company"), + account_type: ["in", ["Receivable", "Payable"]], + }); + }, + }, + { + fieldname: "voucher_no", + label: __("Voucher No"), + fieldtype: "Data", + width: 100, + }, + ]; + return filters; +} + frappe.query_reports["Invalid Ledger Entries"] = { - filters: [ - // { - // "fieldname": "my_filter", - // "label": __("My Filter"), - // "fieldtype": "Data", - // "reqd": 1, - // }, - ], + filters: get_filters(), }; From d51cf281c310caeea61d88a66007f8d7e2d887a1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 9 Sep 2024 17:53:59 +0530 Subject: [PATCH 3/5] refactor: barebones methods with basic logic (cherry picked from commit b05b378ef0fcd4c08404fd541857921702751676) --- .../invalid_ledger_entries.py | 121 +++++++++++++++--- 1 file changed, 105 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py index 4f4b1835227..e1599a6758e 100644 --- a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py @@ -1,8 +1,10 @@ # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe -from frappe import _ +import frappe +from frappe import _, qb +from frappe.query_builder import Criterion +from frappe.query_builder.custom import ConstantColumn def execute(filters: dict | None = None): @@ -12,8 +14,10 @@ def execute(filters: dict | None = None): dictionary and should return columns and data. It is called by the framework every time the report is refreshed or a filter is updated. """ + validate_filters(filters) + columns = get_columns() - data = get_data() + data = get_data(filters) return columns, data @@ -24,25 +28,110 @@ def get_columns() -> list[dict]: One field definition per column, just like a DocType field definition. """ return [ + {"label": _("Voucher Type"), "fieldname": "voucher_type", "fieldtype": "Link", "options": "DocType"}, { - "label": _("Column 1"), - "fieldname": "column_1", - "fieldtype": "Data", - }, - { - "label": _("Column 2"), - "fieldname": "column_2", - "fieldtype": "Int", + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", }, ] -def get_data() -> list[list]: +def get_data(filters) -> list[list]: """Return data for the report. The report data is a list of rows, with each row being a list of cell values. """ - return [ - ["Row 1", 1], - ["Row 2", 2], - ] + active_vouchers = get_active_vouchers_for_period(filters) + invalid_vouchers = identify_cancelled_vouchers(active_vouchers) + + return invalid_vouchers + + +def identify_cancelled_vouchers(active_vouchers: list[dict] | list | None = None) -> list[dict]: + cancelled_vouchers = [] + if active_vouchers: + # Group by voucher types and use single query to identify cancelled vouchers + vtypes = set([x.voucher_type for x in active_vouchers]) + + for _t in vtypes: + _names = [x.voucher_no for x in active_vouchers if x.voucher_type == _t] + dt = qb.DocType(_t) + non_active_vouchers = ( + qb.from_(dt) + .select(ConstantColumn(_t).as_("doctype"), dt.name) + .where(dt.docstatus.ne(1) & dt.name.isin(_names)) + .run() + ) + if non_active_vouchers: + cancelled_vouchers.extend(non_active_vouchers) + return cancelled_vouchers + + +def validate_filters(filters: dict | None = None): + if not filters: + frappe.throw(_("Filters missing")) + + if not filters.company: + frappe.throw(_("Company is mandatory")) + + if filters.from_date > filters.to_date: + frappe.throw(_("Start Date should be lower than End Date")) + + +def build_query_filters(filters: dict | None = None) -> list: + qb_filters = [] + if filters: + if filters.account: + qb_filters.append(qb.Field("account").isin(filters.account)) + + if filters.voucher_no: + qb_filters.append(qb.Field("voucher_no").eq(filters.voucher_no)) + + return qb_filters + + +def get_active_vouchers_for_period(filters: dict | None = None) -> list[dict]: + uniq_vouchers = [] + + if filters: + gle = qb.DocType("GL Entry") + ple = qb.DocType("Payment Ledger Entry") + + qb_filters = build_query_filters(filters) + + gl_vouchers = ( + qb.from_(gle) + .select(gle.voucher_type) + .distinct() + .select(gle.voucher_no) + .distinct() + .where( + gle.is_cancelled.eq(0) + & gle.company.eq(filters.company) + & gle.posting_date[filters.from_date : filters.to_date] + ) + .where(Criterion.all(qb_filters)) + .run(as_dict=True) + ) + + pl_vouchers = ( + qb.from_(ple) + .select(ple.voucher_type) + .distinct() + .select(ple.voucher_no) + .distinct() + .where( + ple.delinked.eq(0) + & ple.company.eq(filters.company) + & ple.posting_date[filters.from_date : filters.to_date] + ) + .where(Criterion.all(qb_filters)) + .run(as_dict=True) + ) + + uniq_vouchers.extend(gl_vouchers) + uniq_vouchers.extend(pl_vouchers) + + return uniq_vouchers From b2d361b495f88d09a6875ebdef20060415d024ce Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Sep 2024 12:58:05 +0530 Subject: [PATCH 4/5] refactor: fetch as dictionary (cherry picked from commit 2126b10a92c38aa12a35bc78b577fc2212d5da6a) --- .../report/invalid_ledger_entries/invalid_ledger_entries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py index e1599a6758e..33fda705cf2 100644 --- a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py @@ -60,9 +60,9 @@ def identify_cancelled_vouchers(active_vouchers: list[dict] | list | None = None dt = qb.DocType(_t) non_active_vouchers = ( qb.from_(dt) - .select(ConstantColumn(_t).as_("doctype"), dt.name) + .select(ConstantColumn(_t).as_("voucher_type"), dt.name.as_("voucher_no")) .where(dt.docstatus.ne(1) & dt.name.isin(_names)) - .run() + .run(as_dict=True) ) if non_active_vouchers: cancelled_vouchers.extend(non_active_vouchers) From 2a890f9061f886844015fe251b829d0b9d0f2857 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Sep 2024 13:03:49 +0530 Subject: [PATCH 5/5] refactor: allow all accounts (cherry picked from commit 43198c946b498ab76ebbcd39de3d5cc031d897db) --- .../report/invalid_ledger_entries/invalid_ledger_entries.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js index ffaaf5d7cbf..47d478f2865 100644 --- a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js @@ -33,7 +33,6 @@ function get_filters() { get_data: function (txt) { return frappe.db.get_link_options("Account", txt, { company: frappe.query_report.get_filter_value("company"), - account_type: ["in", ["Receivable", "Payable"]], }); }, },