feat: ledger comparison report (#36485)

* feat: Accounting Ledger comparison report

* chore: barebones methods

* chore: working state

* chore: refactor internal logic

* chore: working multi select filter on Account

* chore: working voucher no filter

* chore: remove debugging statements

* chore: report with currency symbol

* chore: working start and end date filter

* test: basic report function

* refactor(test): test all filters
This commit is contained in:
ruthra kumar 2023-08-07 11:28:07 +05:30 committed by GitHub
parent 2eea90a873
commit b86747c9d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 405 additions and 0 deletions

View File

@ -0,0 +1,52 @@
// Copyright (c) 2023, 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":"period_start_date",
"label": __("Start Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
},
{
"fieldname":"period_end_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["General and Payment Ledger Comparison"] = {
"filters": get_filters()
};

View File

@ -0,0 +1,32 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2023-08-02 17:30:29.494907",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letterhead": null,
"modified": "2023-08-02 17:30:29.494907",
"modified_by": "Administrator",
"module": "Accounts",
"name": "General and Payment Ledger Comparison",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "General and Payment Ledger Comparison",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
}

View File

@ -0,0 +1,221 @@
# Copyright (c) 2023, 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.functions import Sum
class General_Payment_Ledger_Comparison(object):
"""
A Utility report to compare Voucher-wise balance between General and Payment Ledger
"""
def __init__(self, filters=None):
self.filters = filters
self.gle = []
self.ple = []
def get_accounts(self):
receivable_accounts = [
x[0]
for x in frappe.db.get_all(
"Account",
filters={"company": self.filters.company, "account_type": "Receivable"},
as_list=True,
)
]
payable_accounts = [
x[0]
for x in frappe.db.get_all(
"Account", filters={"company": self.filters.company, "account_type": "Payable"}, as_list=True
)
]
self.account_types = frappe._dict(
{
"receivable": frappe._dict({"accounts": receivable_accounts, "gle": [], "ple": []}),
"payable": frappe._dict({"accounts": payable_accounts, "gle": [], "ple": []}),
}
)
def generate_filters(self):
if self.filters.account:
self.account_types.receivable.accounts = []
self.account_types.payable.accounts = []
for acc in frappe.db.get_all(
"Account", filters={"name": ["in", self.filters.account]}, fields=["name", "account_type"]
):
if acc.account_type == "Receivable":
self.account_types.receivable.accounts.append(acc.name)
else:
self.account_types.payable.accounts.append(acc.name)
def get_gle(self):
gle = qb.DocType("GL Entry")
for acc_type, val in self.account_types.items():
if val.accounts:
filter_criterion = []
if self.filters.voucher_no:
filter_criterion.append((gle.voucher_no == self.filters.voucher_no))
if self.filters.period_start_date:
filter_criterion.append(gle.posting_date.gte(self.filters.period_start_date))
if self.filters.period_end_date:
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
if acc_type == "receivable":
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
else:
outstanding = (Sum(gle.credit) - Sum(gle.debit)).as_("outstanding")
self.account_types[acc_type].gle = (
qb.from_(gle)
.select(
gle.company,
gle.account,
gle.voucher_no,
gle.party,
outstanding,
)
.where(
(gle.company == self.filters.company)
& (gle.is_cancelled == 0)
& (gle.account.isin(val.accounts))
)
.where(Criterion.all(filter_criterion))
.groupby(gle.company, gle.account, gle.voucher_no, gle.party)
.run()
)
def get_ple(self):
ple = qb.DocType("Payment Ledger Entry")
for acc_type, val in self.account_types.items():
if val.accounts:
filter_criterion = []
if self.filters.voucher_no:
filter_criterion.append((ple.voucher_no == self.filters.voucher_no))
if self.filters.period_start_date:
filter_criterion.append(ple.posting_date.gte(self.filters.period_start_date))
if self.filters.period_end_date:
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
self.account_types[acc_type].ple = (
qb.from_(ple)
.select(
ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding")
)
.where(
(ple.company == self.filters.company)
& (ple.delinked == 0)
& (ple.account.isin(val.accounts))
)
.where(Criterion.all(filter_criterion))
.groupby(ple.company, ple.account, ple.voucher_no, ple.party)
.run()
)
def compare(self):
self.gle_balances = set()
self.ple_balances = set()
# consolidate both receivable and payable balances in one set
for acc_type, val in self.account_types.items():
self.gle_balances = set(val.gle) | self.gle_balances
self.ple_balances = set(val.ple) | self.ple_balances
self.diff1 = self.gle_balances.difference(self.ple_balances)
self.diff2 = self.ple_balances.difference(self.gle_balances)
self.diff = frappe._dict({})
for x in self.diff1:
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
for x in self.diff2:
self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]}))
def generate_data(self):
self.data = []
for key, val in self.diff.items():
self.data.append(
frappe._dict(
{
"voucher_no": key[2],
"party": key[3],
"gl_balance": val.gl_balance,
"pl_balance": val.pl_balance,
}
)
)
def get_columns(self):
self.columns = []
options = None
self.columns.append(
dict(
label=_("Voucher No"),
fieldname="voucher_no",
fieldtype="Data",
options=options,
width="100",
)
)
self.columns.append(
dict(
label=_("Party"),
fieldname="party",
fieldtype="Data",
options=options,
width="100",
)
)
self.columns.append(
dict(
label=_("GL Balance"),
fieldname="gl_balance",
fieldtype="Currency",
options="Company:company:default_currency",
width="100",
)
)
self.columns.append(
dict(
label=_("Payment Ledger Balance"),
fieldname="pl_balance",
fieldtype="Currency",
options="Company:company:default_currency",
width="100",
)
)
def run(self):
self.get_accounts()
self.generate_filters()
self.get_gle()
self.get_ple()
self.compare()
self.generate_data()
self.get_columns()
return self.columns, self.data
def execute(filters=None):
columns, data = [], []
rpt = General_Payment_Ledger_Comparison(filters)
columns, data = rpt.run()
return columns, data

View File

@ -0,0 +1,100 @@
import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.general_and_payment_ledger_comparison.general_and_payment_ledger_comparison import (
execute,
)
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin):
def setUp(self):
self.create_company()
self.cleanup()
def tearDown(self):
frappe.db.rollback()
def cleanup(self):
doctypes = []
doctypes.append(qb.DocType("GL Entry"))
doctypes.append(qb.DocType("Payment Ledger Entry"))
doctypes.append(qb.DocType("Sales Invoice"))
for doctype in doctypes:
qb.from_(doctype).delete().where(doctype.company == self.company).run()
def test_01_basic_report_functionality(self):
sinv = create_sales_invoice(
company=self.company,
debit_to=self.debit_to,
expense_account=self.expense_account,
cost_center=self.cost_center,
income_account=self.income_account,
warehouse=self.warehouse,
)
# manually edit the payment ledger entry
ple = frappe.db.get_all(
"Payment Ledger Entry", filters={"voucher_no": sinv.name, "delinked": 0}
)[0]
frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", sinv.grand_total - 1)
filters = frappe._dict({"company": self.company})
columns, data = execute(filters=filters)
self.assertEqual(len(data), 1)
expected = {
"voucher_no": sinv.name,
"party": sinv.customer,
"gl_balance": sinv.grand_total,
"pl_balance": sinv.grand_total - 1,
}
self.assertEqual(expected, data[0])
# account filter
filters = frappe._dict({"company": self.company, "account": self.debit_to})
columns, data = execute(filters=filters)
self.assertEqual(len(data), 1)
self.assertEqual(expected, data[0])
filters = frappe._dict({"company": self.company, "account": self.creditors})
columns, data = execute(filters=filters)
self.assertEqual([], data)
# voucher_no filter
filters = frappe._dict({"company": self.company, "voucher_no": sinv.name})
columns, data = execute(filters=filters)
self.assertEqual(len(data), 1)
self.assertEqual(expected, data[0])
filters = frappe._dict({"company": self.company, "voucher_no": sinv.name + "-1"})
columns, data = execute(filters=filters)
self.assertEqual([], data)
# date range filter
filters = frappe._dict(
{
"company": self.company,
"period_start_date": sinv.posting_date,
"period_end_date": sinv.posting_date,
}
)
columns, data = execute(filters=filters)
self.assertEqual(len(data), 1)
self.assertEqual(expected, data[0])
filters = frappe._dict(
{
"company": self.company,
"period_start_date": add_days(sinv.posting_date, -1),
"period_end_date": add_days(sinv.posting_date, -1),
}
)
columns, data = execute(filters=filters)
self.assertEqual([], data)