diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js new file mode 100644 index 0000000000..7e6b0537e8 --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js @@ -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() +}; diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json new file mode 100644 index 0000000000..1d0d9d134d --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py new file mode 100644 index 0000000000..553c137f02 --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py @@ -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 diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py new file mode 100644 index 0000000000..4b0e99d712 --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py @@ -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)