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:
		
							parent
							
								
									2eea90a873
								
							
						
					
					
						commit
						b86747c9d4
					
				| @ -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() | ||||||
|  | }; | ||||||
| @ -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" | ||||||
|  |   } | ||||||
|  |  ] | ||||||
|  | } | ||||||
| @ -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 | ||||||
| @ -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) | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user