mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-23 15:09:20 +00:00
Merge pull request #43144 from ruthra-kumar/utility_report_for_invalid_ledger_entries
feat: utility report for identifying invalid ledger entries
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
// 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"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "voucher_no",
|
||||||
|
label: __("Voucher No"),
|
||||||
|
fieldtype: "Data",
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.query_reports["Invalid Ledger Entries"] = {
|
||||||
|
filters: get_filters(),
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""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.
|
||||||
|
"""
|
||||||
|
validate_filters(filters)
|
||||||
|
|
||||||
|
columns = get_columns()
|
||||||
|
data = get_data(filters)
|
||||||
|
|
||||||
|
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": _("Voucher Type"), "fieldname": "voucher_type", "fieldtype": "Link", "options": "DocType"},
|
||||||
|
{
|
||||||
|
"label": _("Voucher No"),
|
||||||
|
"fieldname": "voucher_no",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"options": "voucher_type",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
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_("voucher_type"), dt.name.as_("voucher_no"))
|
||||||
|
.where(dt.docstatus.ne(1) & dt.name.isin(_names))
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user