Merge branch 'develop' into refactor_payment_reconciliation_tool
diff --git a/erpnext/accounts/README.md b/erpnext/accounts/README.md
index da1f201..15f7039 100644
--- a/erpnext/accounts/README.md
+++ b/erpnext/accounts/README.md
@@ -10,4 +10,42 @@
 - Sales Invoice (Itemised)
 - Purchase Invoice (Itemised)
 
-All accounting entries are stored in the `General Ledger`
\ No newline at end of file
+All accounting entries are stored in the `General Ledger`
+
+## Payment Ledger
+Transactions on Receivable and Payable Account types will also be stored in `Payment Ledger`. This is so that payment reconciliation process only requires update on this ledger.
+
+### Key Fields
+| Field                | Description                      |
+|----------------------|----------------------------------|
+| `account_type`       | Receivable/Payable               |
+| `account`            | Accounting head                  |
+| `party`              | Party Name                       |
+| `voucher_no`         | Voucher No                       |
+| `against_voucher_no` | Linked voucher(secondary effect) |
+| `amount`             | can be +ve/-ve                   |
+
+### Design
+`debit` and `credit` have been replaced with `account_type` and `amount`. `against_voucher_no` is populated for all entries. So, outstanding amount can be calculated by summing up amount only using `against_voucher_no`.
+
+Ex:
+1. Consider an invoice for ₹100 and a partial payment of ₹80 against that invoice. Payment Ledger will have following entries.
+
+| voucher_no | against_voucher_no | amount |
+|------------|--------------------|--------|
+| SINV-01    | SINV-01            | 100    |
+| PAY-01     | SINV-01            | -80    |
+
+
+2. Reconcile a Credit Note against an invoice using a Journal Entry
+
+An invoice for ₹100 partially reconciled against a credit of ₹70 using a Journal Entry. Payment Ledger will have the following entries.
+
+| voucher_no | against_voucher_no | amount |
+|------------|--------------------|--------|
+| SINV-01    | SINV-01            | 100    |
+|            |                    |        |
+| CR-NOTE-01 | CR-NOTE-01         | -70    |
+|            |                    |        |
+| JE-01      | CR-NOTE-01         | +70    |
+| JE-01      | SINV-01            | -70    |
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index e5fa57d..9f71656 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -58,16 +58,20 @@
 			validate_balance_type(self.account, adv_adj)
 			validate_frozen_account(self.account, adv_adj)
 
-			# Update outstanding amt on against voucher
-			if (
-				self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
-				and self.against_voucher
-				and self.flags.update_outstanding == "Yes"
-				and not frappe.flags.is_reverse_depr_entry
-			):
-				update_outstanding_amt(
-					self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
-				)
+			if frappe.db.get_value("Account", self.account, "account_type") not in [
+				"Receivable",
+				"Payable",
+			]:
+				# Update outstanding amt on against voucher
+				if (
+					self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
+					and self.against_voucher
+					and self.flags.update_outstanding == "Yes"
+					and not frappe.flags.is_reverse_depr_entry
+				):
+					update_outstanding_amt(
+						self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
+					)
 
 	def check_mandatory(self):
 		mandatory = ["account", "voucher_type", "voucher_no", "company"]
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 2c16ca3..787efd2 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -800,9 +800,7 @@
 
 		self.total_amount_in_words = money_in_words(amt, currency)
 
-	def make_gl_entries(self, cancel=0, adv_adj=0):
-		from erpnext.accounts.general_ledger import make_gl_entries
-
+	def build_gl_map(self):
 		gl_map = []
 		for d in self.get("accounts"):
 			if d.debit or d.credit:
@@ -838,7 +836,12 @@
 						item=d,
 					)
 				)
+		return gl_map
 
+	def make_gl_entries(self, cancel=0, adv_adj=0):
+		from erpnext.accounts.general_ledger import make_gl_entries
+
+		gl_map = self.build_gl_map()
 		if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
 			update_outstanding = "No"
 		else:
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index f7a57bb..d8af9db 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -6,7 +6,7 @@
 from functools import reduce
 
 import frappe
-from frappe import ValidationError, _, scrub, throw
+from frappe import ValidationError, _, qb, scrub, throw
 from frappe.utils import cint, comma_or, flt, getdate, nowdate
 
 import erpnext
@@ -785,7 +785,7 @@
 
 		self.set("remarks", "\n".join(remarks))
 
-	def make_gl_entries(self, cancel=0, adv_adj=0):
+	def build_gl_map(self):
 		if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
 			self.setup_party_account_field()
 
@@ -794,7 +794,10 @@
 		self.add_bank_gl_entries(gl_entries)
 		self.add_deductions_gl_entries(gl_entries)
 		self.add_tax_gl_entries(gl_entries)
+		return gl_entries
 
+	def make_gl_entries(self, cancel=0, adv_adj=0):
+		gl_entries = self.build_gl_map()
 		gl_entries = process_gl_map(gl_entries)
 		make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
 
@@ -1195,6 +1198,9 @@
 	if args.get("party_type") == "Member":
 		return
 
+	ple = qb.DocType("Payment Ledger Entry")
+	common_filter = []
+
 	# confirm that Supplier is not blocked
 	if args.get("party_type") == "Supplier":
 		supplier_status = get_supplier_block_status(args["party"])
@@ -1216,10 +1222,13 @@
 		condition = " and voucher_type={0} and voucher_no={1}".format(
 			frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
 		)
+		common_filter.append(ple.voucher_type == args["voucher_type"])
+		common_filter.append(ple.voucher_no == args["voucher_no"])
 
 	# Add cost center condition
 	if args.get("cost_center"):
 		condition += " and cost_center='%s'" % args.get("cost_center")
+		common_filter.append(ple.cost_center == args.get("cost_center"))
 
 	date_fields_dict = {
 		"posting_date": ["from_posting_date", "to_posting_date"],
@@ -1231,16 +1240,19 @@
 			condition += " and {0} between '{1}' and '{2}'".format(
 				fieldname, args.get(date_fields[0]), args.get(date_fields[1])
 			)
+			common_filter.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
 
 	if args.get("company"):
 		condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
+		common_filter.append(ple.company == args.get("company"))
 
 	outstanding_invoices = get_outstanding_invoices(
 		args.get("party_type"),
 		args.get("party"),
 		args.get("party_account"),
-		filters=args,
-		condition=condition,
+		common_filter=common_filter,
+		min_outstanding=args.get("outstanding_amt_greater_than"),
+		max_outstanding=args.get("outstanding_amt_less_than"),
 	)
 
 	outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py
index 43e19f4..52df923 100644
--- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py
@@ -6,6 +6,19 @@
 from frappe import _
 from frappe.model.document import Document
 
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+	get_checks_for_pl_and_bs_accounts,
+)
+from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
+	get_dimension_filter_map,
+)
+from erpnext.accounts.doctype.gl_entry.gl_entry import (
+	validate_balance_type,
+	validate_frozen_account,
+)
+from erpnext.accounts.utils import update_voucher_outstanding
+from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
+
 
 class PaymentLedgerEntry(Document):
 	def validate_account(self):
@@ -18,5 +31,119 @@
 		if not valid_account:
 			frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
 
+	def validate_account_details(self):
+		"""Account must be ledger, active and not freezed"""
+
+		ret = frappe.db.sql(
+			"""select is_group, docstatus, company
+			from tabAccount where name=%s""",
+			self.account,
+			as_dict=1,
+		)[0]
+
+		if ret.is_group == 1:
+			frappe.throw(
+				_(
+					"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
+				).format(self.voucher_type, self.voucher_no, self.account)
+			)
+
+		if ret.docstatus == 2:
+			frappe.throw(
+				_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
+			)
+
+		if ret.company != self.company:
+			frappe.throw(
+				_("{0} {1}: Account {2} does not belong to Company {3}").format(
+					self.voucher_type, self.voucher_no, self.account, self.company
+				)
+			)
+
+	def validate_allowed_dimensions(self):
+		dimension_filter_map = get_dimension_filter_map()
+		for key, value in dimension_filter_map.items():
+			dimension = key[0]
+			account = key[1]
+
+			if self.account == account:
+				if value["is_mandatory"] and not self.get(dimension):
+					frappe.throw(
+						_("{0} is mandatory for account {1}").format(
+							frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
+						),
+						MandatoryAccountDimensionError,
+					)
+
+				if value["allow_or_restrict"] == "Allow":
+					if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
+						frappe.throw(
+							_("Invalid value {0} for {1} against account {2}").format(
+								frappe.bold(self.get(dimension)),
+								frappe.bold(frappe.unscrub(dimension)),
+								frappe.bold(self.account),
+							),
+							InvalidAccountDimensionError,
+						)
+				else:
+					if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
+						frappe.throw(
+							_("Invalid value {0} for {1} against account {2}").format(
+								frappe.bold(self.get(dimension)),
+								frappe.bold(frappe.unscrub(dimension)),
+								frappe.bold(self.account),
+							),
+							InvalidAccountDimensionError,
+						)
+
+	def validate_dimensions_for_pl_and_bs(self):
+		account_type = frappe.db.get_value("Account", self.account, "report_type")
+
+		for dimension in get_checks_for_pl_and_bs_accounts():
+			if (
+				account_type == "Profit and Loss"
+				and self.company == dimension.company
+				and dimension.mandatory_for_pl
+				and not dimension.disabled
+			):
+				if not self.get(dimension.fieldname):
+					frappe.throw(
+						_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.").format(
+							dimension.label, self.account
+						)
+					)
+
+			if (
+				account_type == "Balance Sheet"
+				and self.company == dimension.company
+				and dimension.mandatory_for_bs
+				and not dimension.disabled
+			):
+				if not self.get(dimension.fieldname):
+					frappe.throw(
+						_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.").format(
+							dimension.label, self.account
+						)
+					)
+
 	def validate(self):
 		self.validate_account()
+
+	def on_update(self):
+		adv_adj = self.flags.adv_adj
+		if not self.flags.from_repost:
+			self.validate_account_details()
+			self.validate_dimensions_for_pl_and_bs()
+			self.validate_allowed_dimensions()
+			validate_balance_type(self.account, adv_adj)
+			validate_frozen_account(self.account, adv_adj)
+
+		# update outstanding amount
+		if (
+			self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
+			and self.flags.update_outstanding == "Yes"
+			and not frappe.flags.is_reverse_depr_entry
+		):
+			update_voucher_outstanding(
+				self.against_voucher_type, self.against_voucher_no, self.account, self.party_type, self.party
+			)
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index e5b942f..5b2b526 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -3,16 +3,26 @@
 
 
 import frappe
-from frappe import _, msgprint
+from frappe import _, msgprint, qb
 from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
+from frappe.query_builder.functions import IfNull
 from frappe.utils import flt, getdate, nowdate, today
 
 import erpnext
-from erpnext.accounts.utils import get_outstanding_invoices, reconcile_against_document
+from erpnext.accounts.utils import (
+	QueryPaymentLedger,
+	get_outstanding_invoices,
+	reconcile_against_document,
+)
 from erpnext.controllers.accounts_controller import get_advance_payment_entries
 
 
 class PaymentReconciliation(Document):
+	def __init__(self, *args, **kwargs):
+		super(PaymentReconciliation, self).__init__(*args, **kwargs)
+		self.common_filter_conditions = []
+
 	@frappe.whitelist()
 	def get_unreconciled_entries(self):
 		self.get_nonreconciled_payment_entries()
@@ -108,54 +118,58 @@
 		return list(journal_entries)
 
 	def get_dr_or_cr_notes(self):
-		condition = self.get_conditions(get_return_invoices=True)
-		dr_or_cr = (
-			"credit_in_account_currency"
-			if erpnext.get_party_account_type(self.party_type) == "Receivable"
-			else "debit_in_account_currency"
-		)
 
-		reconciled_dr_or_cr = (
-			"debit_in_account_currency"
-			if dr_or_cr == "credit_in_account_currency"
-			else "credit_in_account_currency"
-		)
+		self.build_qb_filter_conditions(get_return_invoices=True)
 
+		ple = qb.DocType("Payment Ledger Entry")
 		voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
 
-		return frappe.db.sql(
-			""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
-				(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
-				account_currency as currency
-			FROM `tab{doc}` doc, `tabGL Entry` gl
-			WHERE
-				(doc.name = gl.against_voucher or doc.name = gl.voucher_no)
-				and doc.{party_type_field} = %(party)s
-				and doc.is_return = 1 and ifnull(doc.return_against, "") = ""
-				and gl.against_voucher_type = %(voucher_type)s
-				and doc.docstatus = 1 and gl.party = %(party)s
-				and gl.party_type = %(party_type)s and gl.account = %(account)s
-				and gl.is_cancelled = 0 {condition}
-			GROUP BY doc.name
-			Having
-				amount > 0
-			ORDER BY doc.posting_date
-		""".format(
-				doc=voucher_type,
-				dr_or_cr=dr_or_cr,
-				reconciled_dr_or_cr=reconciled_dr_or_cr,
-				party_type_field=frappe.scrub(self.party_type),
-				condition=condition or "",
-			),
-			{
-				"party": self.party,
-				"party_type": self.party_type,
-				"voucher_type": voucher_type,
-				"account": self.receivable_payable_account,
-			},
-			as_dict=1,
+		if erpnext.get_party_account_type(self.party_type) == "Receivable":
+			self.common_filter_conditions.append(ple.account_type == "Receivable")
+		else:
+			self.common_filter_conditions.append(ple.account_type == "Payable")
+		self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
+
+		# get return invoices
+		doc = qb.DocType(voucher_type)
+		return_invoices = (
+			qb.from_(doc)
+			.select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no"))
+			.where(
+				(doc.docstatus == 1)
+				& (doc[frappe.scrub(self.party_type)] == self.party)
+				& (doc.is_return == 1)
+				& (IfNull(doc.return_against, "") == "")
+			)
+			.run(as_dict=True)
 		)
 
+		outstanding_dr_or_cr = []
+		if return_invoices:
+			ple_query = QueryPaymentLedger()
+			return_outstanding = ple_query.get_voucher_outstandings(
+				vouchers=return_invoices,
+				common_filter=self.common_filter_conditions,
+				min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
+				max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
+				get_payments=True,
+			)
+
+			for inv in return_outstanding:
+				if inv.outstanding != 0:
+					outstanding_dr_or_cr.append(
+						frappe._dict(
+							{
+								"reference_type": inv.voucher_type,
+								"reference_name": inv.voucher_no,
+								"amount": -(inv.outstanding),
+								"posting_date": inv.posting_date,
+								"currency": inv.currency,
+							}
+						)
+					)
+		return outstanding_dr_or_cr
+
 	def add_payment_entries(self, non_reconciled_payments):
 		self.set("payments", [])
 
@@ -166,10 +180,15 @@
 	def get_invoice_entries(self):
 		# Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
 
-		condition = self.get_conditions(get_invoices=True)
+		self.build_qb_filter_conditions(get_invoices=True)
 
 		non_reconciled_invoices = get_outstanding_invoices(
-			self.party_type, self.party, self.receivable_payable_account, condition=condition
+			self.party_type,
+			self.party,
+			self.receivable_payable_account,
+			common_filter=self.common_filter_conditions,
+			min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
+			max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
 		)
 
 		if self.invoice_limit:
@@ -329,89 +348,56 @@
 		if not invoices_to_reconcile:
 			frappe.throw(_("No records found in Allocation table"))
 
-	def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
-		condition = " and company = '{0}' ".format(self.company)
+	def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
+		self.common_filter_conditions.clear()
+		ple = qb.DocType("Payment Ledger Entry")
 
-		if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
-			condition = " and cost_center = '{0}' ".format(self.cost_center)
+		self.common_filter_conditions.append(ple.company == self.company)
+
+		if self.get("cost_center") and (get_invoices or get_return_invoices):
+			self.common_filter_conditions.append(ple.cost_center == self.cost_center)
 
 		if get_invoices:
-			condition += (
-				" and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
-				if self.from_invoice_date
-				else ""
-			)
-			condition += (
-				" and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date))
-				if self.to_invoice_date
-				else ""
-			)
-			dr_or_cr = (
-				"debit_in_account_currency"
-				if erpnext.get_party_account_type(self.party_type) == "Receivable"
-				else "credit_in_account_currency"
-			)
-
-			if self.minimum_invoice_amount:
-				condition += " and {dr_or_cr} >= {amount}".format(
-					dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount)
-				)
-			if self.maximum_invoice_amount:
-				condition += " and {dr_or_cr} <= {amount}".format(
-					dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
-				)
+			if self.from_invoice_date:
+				self.common_filter_conditions.append(ple.posting_date.gte(self.from_invoice_date))
+			if self.to_invoice_date:
+				self.common_filter_conditions.append(ple.posting_date.lte(self.to_invoice_date))
 
 		elif get_return_invoices:
-			condition = " and doc.company = '{0}' ".format(self.company)
-			condition += (
-				" and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
-				if self.from_payment_date
-				else ""
-			)
-			condition += (
-				" and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
-				if self.to_payment_date
-				else ""
-			)
-			dr_or_cr = (
-				"debit_in_account_currency"
-				if erpnext.get_party_account_type(self.party_type) == "Receivable"
-				else "credit_in_account_currency"
-			)
+			if self.from_payment_date:
+				self.common_filter_conditions.append(ple.posting_date.gte(self.from_payment_date))
+			if self.to_payment_date:
+				self.common_filter_conditions.append(ple.posting_date.lte(self.to_payment_date))
 
-			if self.minimum_invoice_amount:
-				condition += " and gl.{dr_or_cr} >= {amount}".format(
-					dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
-				)
-			if self.maximum_invoice_amount:
-				condition += " and gl.{dr_or_cr} <= {amount}".format(
-					dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
-				)
+	def get_conditions(self, get_payments=False):
+		condition = " and company = '{0}' ".format(self.company)
 
-		else:
-			condition += (
-				" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
-				if self.from_payment_date
-				else ""
-			)
-			condition += (
-				" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
-				if self.to_payment_date
-				else ""
-			)
+		if self.get("cost_center") and get_payments:
+			condition = " and cost_center = '{0}' ".format(self.cost_center)
 
-			if self.minimum_payment_amount:
-				condition += (
-					" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
-					if get_payments
-					else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
-				)
-			if self.maximum_payment_amount:
-				condition += (
-					" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
-					if get_payments
-					else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
-				)
+		condition += (
+			" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
+			if self.from_payment_date
+			else ""
+		)
+		condition += (
+			" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
+			if self.to_payment_date
+			else ""
+		)
+
+		if self.minimum_payment_amount:
+			condition += (
+				" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
+				if get_payments
+				else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
+			)
+		if self.maximum_payment_amount:
+			condition += (
+				" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
+				if get_payments
+				else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
+			)
 
 		return condition
 
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index d2374b7..575ac74 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -4,93 +4,453 @@
 import unittest
 
 import frappe
-from frappe.utils import add_days, getdate
+from frappe import qb
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, nowdate
 
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
 from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.party import get_party_account
+from erpnext.stock.doctype.item.test_item import create_item
 
 
-class TestPaymentReconciliation(unittest.TestCase):
-	@classmethod
-	def setUpClass(cls):
-		make_customer()
-		make_invoice_and_payment()
+class TestPaymentReconciliation(FrappeTestCase):
+	def setUp(self):
+		self.create_company()
+		self.create_item()
+		self.create_customer()
+		self.clear_old_entries()
 
-	def test_payment_reconciliation(self):
-		payment_reco = frappe.get_doc("Payment Reconciliation")
-		payment_reco.company = "_Test Company"
-		payment_reco.party_type = "Customer"
-		payment_reco.party = "_Test Payment Reco Customer"
-		payment_reco.receivable_payable_account = "Debtors - _TC"
-		payment_reco.from_invoice_date = add_days(getdate(), -1)
-		payment_reco.to_invoice_date = getdate()
-		payment_reco.from_payment_date = add_days(getdate(), -1)
-		payment_reco.to_payment_date = getdate()
-		payment_reco.maximum_invoice_amount = 1000
-		payment_reco.maximum_payment_amount = 1000
-		payment_reco.invoice_limit = 10
-		payment_reco.payment_limit = 10
-		payment_reco.bank_cash_account = "_Test Bank - _TC"
-		payment_reco.cost_center = "_Test Cost Center - _TC"
-		payment_reco.get_unreconciled_entries()
+	def tearDown(self):
+		frappe.db.rollback()
 
-		self.assertEqual(len(payment_reco.get("invoices")), 1)
-		self.assertEqual(len(payment_reco.get("payments")), 1)
+	def create_company(self):
+		company = None
+		if frappe.db.exists("Company", "_Test Payment Reconciliation"):
+			company = frappe.get_doc("Company", "_Test Payment Reconciliation")
+		else:
+			company = frappe.get_doc(
+				{
+					"doctype": "Company",
+					"company_name": "_Test Payment Reconciliation",
+					"country": "India",
+					"default_currency": "INR",
+					"create_chart_of_accounts_based_on": "Standard Template",
+					"chart_of_accounts": "Standard",
+				}
+			)
+			company = company.save()
 
-		payment_entry = payment_reco.get("payments")[0].reference_name
-		invoice = payment_reco.get("invoices")[0].invoice_number
+		self.company = company.name
+		self.cost_center = company.cost_center
+		self.warehouse = "All Warehouses - _PR"
+		self.income_account = "Sales - _PR"
+		self.expense_account = "Cost of Goods Sold - _PR"
+		self.debit_to = "Debtors - _PR"
+		self.creditors = "Creditors - _PR"
 
-		payment_reco.allocate_entries(
-			{
-				"payments": [payment_reco.get("payments")[0].as_dict()],
-				"invoices": [payment_reco.get("invoices")[0].as_dict()],
-			}
+		# create bank account
+		if frappe.db.exists("Account", "HDFC - _PR"):
+			self.bank = "HDFC - _PR"
+		else:
+			bank_acc = frappe.get_doc(
+				{
+					"doctype": "Account",
+					"account_name": "HDFC",
+					"parent_account": "Bank Accounts - _PR",
+					"company": self.company,
+				}
+			)
+			bank_acc.save()
+			self.bank = bank_acc.name
+
+	def create_item(self):
+		item = create_item(
+			item_code="_Test PR Item", is_stock_item=0, company=self.company, warehouse=self.warehouse
 		)
-		payment_reco.reconcile()
+		self.item = item if isinstance(item, str) else item.item_code
 
-		payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry)
-		self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice)
+	def create_customer(self):
+		if frappe.db.exists("Customer", "_Test PR Customer"):
+			self.customer = "_Test PR Customer"
+		else:
+			customer = frappe.new_doc("Customer")
+			customer.customer_name = "_Test PR Customer"
+			customer.type = "Individual"
+			customer.save()
+			self.customer = customer.name
 
+		if frappe.db.exists("Customer", "_Test PR Customer 2"):
+			self.customer2 = "_Test PR Customer 2"
+		else:
+			customer = frappe.new_doc("Customer")
+			customer.customer_name = "_Test PR Customer 2"
+			customer.type = "Individual"
+			customer.save()
+			self.customer2 = customer.name
 
-def make_customer():
-	if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"):
-		frappe.get_doc(
-			{
-				"doctype": "Customer",
-				"customer_name": "_Test Payment Reco Customer",
-				"customer_type": "Individual",
-				"customer_group": "_Test Customer Group",
-				"territory": "_Test Territory",
-			}
-		).insert()
+	def create_sales_invoice(
+		self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
+	):
+		"""
+		Helper function to populate default values in sales invoice
+		"""
+		sinv = create_sales_invoice(
+			qty=qty,
+			rate=rate,
+			company=self.company,
+			customer=self.customer,
+			item_code=self.item,
+			item_name=self.item,
+			cost_center=self.cost_center,
+			warehouse=self.warehouse,
+			debit_to=self.debit_to,
+			parent_cost_center=self.cost_center,
+			update_stock=0,
+			currency="INR",
+			is_pos=0,
+			is_return=0,
+			return_against=None,
+			income_account=self.income_account,
+			expense_account=self.expense_account,
+			do_not_save=do_not_save,
+			do_not_submit=do_not_submit,
+		)
+		return sinv
 
+	def create_payment_entry(self, amount=100, posting_date=nowdate()):
+		"""
+		Helper function to populate default values in payment entry
+		"""
+		payment = create_payment_entry(
+			company=self.company,
+			payment_type="Receive",
+			party_type="Customer",
+			party=self.customer,
+			paid_from=self.debit_to,
+			paid_to=self.bank,
+			paid_amount=amount,
+		)
+		payment.posting_date = posting_date
+		return payment
 
-def make_invoice_and_payment():
-	si = create_sales_invoice(
-		customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True
-	)
-	si.cost_center = "_Test Cost Center - _TC"
-	si.save()
-	si.submit()
+	def clear_old_entries(self):
+		doctype_list = [
+			"GL Entry",
+			"Payment Ledger Entry",
+			"Sales Invoice",
+			"Purchase Invoice",
+			"Payment Entry",
+			"Journal Entry",
+		]
+		for doctype in doctype_list:
+			qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
 
-	pe = frappe.get_doc(
-		{
-			"doctype": "Payment Entry",
-			"payment_type": "Receive",
-			"party_type": "Customer",
-			"party": "_Test Payment Reco Customer",
-			"company": "_Test Company",
-			"paid_from_account_currency": "INR",
-			"paid_to_account_currency": "INR",
-			"source_exchange_rate": 1,
-			"target_exchange_rate": 1,
-			"reference_no": "1",
-			"reference_date": getdate(),
-			"received_amount": 690,
-			"paid_amount": 690,
-			"paid_from": "Debtors - _TC",
-			"paid_to": "_Test Bank - _TC",
-			"cost_center": "_Test Cost Center - _TC",
-		}
-	)
-	pe.insert()
-	pe.submit()
+	def create_payment_reconciliation(self):
+		pr = frappe.new_doc("Payment Reconciliation")
+		pr.company = self.company
+		pr.party_type = "Customer"
+		pr.party = self.customer
+		pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
+		pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
+		return pr
+
+	def create_journal_entry(
+		self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
+	):
+		je = frappe.new_doc("Journal Entry")
+		je.posting_date = posting_date or nowdate()
+		je.company = self.company
+		je.user_remark = "test"
+		if not cost_center:
+			cost_center = self.cost_center
+		je.set(
+			"accounts",
+			[
+				{
+					"account": acc1,
+					"cost_center": cost_center,
+					"debit_in_account_currency": amount if amount > 0 else 0,
+					"credit_in_account_currency": abs(amount) if amount < 0 else 0,
+				},
+				{
+					"account": acc2,
+					"cost_center": cost_center,
+					"credit_in_account_currency": amount if amount > 0 else 0,
+					"debit_in_account_currency": abs(amount) if amount < 0 else 0,
+				},
+			],
+		)
+		return je
+
+	def test_filter_min_max(self):
+		# check filter condition minimum and maximum amount
+		self.create_sales_invoice(qty=1, rate=300)
+		self.create_sales_invoice(qty=1, rate=400)
+		self.create_sales_invoice(qty=1, rate=500)
+		self.create_payment_entry(amount=300).save().submit()
+		self.create_payment_entry(amount=400).save().submit()
+		self.create_payment_entry(amount=500).save().submit()
+
+		pr = self.create_payment_reconciliation()
+		pr.minimum_invoice_amount = 400
+		pr.maximum_invoice_amount = 500
+		pr.minimum_payment_amount = 300
+		pr.maximum_payment_amount = 600
+		pr.get_unreconciled_entries()
+		self.assertEqual(len(pr.get("invoices")), 2)
+		self.assertEqual(len(pr.get("payments")), 3)
+
+		pr.minimum_invoice_amount = 300
+		pr.maximum_invoice_amount = 600
+		pr.minimum_payment_amount = 400
+		pr.maximum_payment_amount = 500
+		pr.get_unreconciled_entries()
+		self.assertEqual(len(pr.get("invoices")), 3)
+		self.assertEqual(len(pr.get("payments")), 2)
+
+		pr.minimum_invoice_amount = (
+			pr.maximum_invoice_amount
+		) = pr.minimum_payment_amount = pr.maximum_payment_amount = 0
+		pr.get_unreconciled_entries()
+		self.assertEqual(len(pr.get("invoices")), 3)
+		self.assertEqual(len(pr.get("payments")), 3)
+
+	def test_filter_posting_date(self):
+		# check filter condition using transaction date
+		date1 = nowdate()
+		date2 = add_days(nowdate(), -1)
+		amount = 100
+		self.create_sales_invoice(qty=1, rate=amount, posting_date=date1)
+		si2 = self.create_sales_invoice(
+			qty=1, rate=amount, posting_date=date2, do_not_save=True, do_not_submit=True
+		)
+		si2.set_posting_time = 1
+		si2.posting_date = date2
+		si2.save().submit()
+		self.create_payment_entry(amount=amount, posting_date=date1).save().submit()
+		self.create_payment_entry(amount=amount, posting_date=date2).save().submit()
+
+		pr = self.create_payment_reconciliation()
+		pr.from_invoice_date = pr.to_invoice_date = date1
+		pr.from_payment_date = pr.to_payment_date = date1
+
+		pr.get_unreconciled_entries()
+		# assert only si and pe are fetched
+		self.assertEqual(len(pr.get("invoices")), 1)
+		self.assertEqual(len(pr.get("payments")), 1)
+
+		pr.from_invoice_date = date2
+		pr.to_invoice_date = date1
+		pr.from_payment_date = date2
+		pr.to_payment_date = date1
+
+		pr.get_unreconciled_entries()
+		# assert only si and pe are fetched
+		self.assertEqual(len(pr.get("invoices")), 2)
+		self.assertEqual(len(pr.get("payments")), 2)
+
+	def test_filter_invoice_limit(self):
+		# check filter condition - invoice limit
+		transaction_date = nowdate()
+		rate = 100
+		invoices = []
+		payments = []
+		for i in range(5):
+			invoices.append(self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date))
+			pe = self.create_payment_entry(amount=rate, posting_date=transaction_date).save().submit()
+			payments.append(pe)
+
+		pr = self.create_payment_reconciliation()
+		pr.from_invoice_date = pr.to_invoice_date = transaction_date
+		pr.from_payment_date = pr.to_payment_date = transaction_date
+		pr.invoice_limit = 2
+		pr.payment_limit = 3
+		pr.get_unreconciled_entries()
+
+		self.assertEqual(len(pr.get("invoices")), 2)
+		self.assertEqual(len(pr.get("payments")), 3)
+
+	def test_payment_against_invoice(self):
+		si = self.create_sales_invoice(qty=1, rate=200)
+		pe = self.create_payment_entry(amount=55).save().submit()
+		# second payment entry
+		self.create_payment_entry(amount=35).save().submit()
+
+		pr = self.create_payment_reconciliation()
+
+		# reconcile multiple payments against invoice
+		pr.get_unreconciled_entries()
+		invoices = [x.as_dict() for x in pr.get("invoices")]
+		payments = [x.as_dict() for x in pr.get("payments")]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		pr.reconcile()
+
+		si.reload()
+		self.assertEqual(si.status, "Partly Paid")
+		# check PR tool output post reconciliation
+		self.assertEqual(len(pr.get("invoices")), 1)
+		self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 110)
+		self.assertEqual(pr.get("payments"), [])
+
+		# cancel one PE
+		pe.reload()
+		pe.cancel()
+		pr.get_unreconciled_entries()
+		# check PR tool output
+		self.assertEqual(len(pr.get("invoices")), 1)
+		self.assertEqual(len(pr.get("payments")), 0)
+		self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 165)
+
+	def test_payment_against_journal(self):
+		transaction_date = nowdate()
+
+		sales = "Sales - _PR"
+		amount = 921
+		# debit debtors account to record an invoice
+		je = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
+		je.accounts[0].party_type = "Customer"
+		je.accounts[0].party = self.customer
+		je.save()
+		je.submit()
+
+		self.create_payment_entry(amount=amount, posting_date=transaction_date).save().submit()
+
+		pr = self.create_payment_reconciliation()
+		pr.minimum_invoice_amount = pr.maximum_invoice_amount = amount
+		pr.from_invoice_date = pr.to_invoice_date = transaction_date
+		pr.from_payment_date = pr.to_payment_date = transaction_date
+
+		pr.get_unreconciled_entries()
+		invoices = [x.as_dict() for x in pr.get("invoices")]
+		payments = [x.as_dict() for x in pr.get("payments")]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		pr.reconcile()
+
+		# check PR tool output
+		self.assertEqual(len(pr.get("invoices")), 0)
+		self.assertEqual(len(pr.get("payments")), 0)
+
+	def test_journal_against_invoice(self):
+		transaction_date = nowdate()
+		amount = 100
+		si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+
+		# credit debtors account to record a payment
+		je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
+		je.accounts[1].party_type = "Customer"
+		je.accounts[1].party = self.customer
+		je.save()
+		je.submit()
+
+		pr = self.create_payment_reconciliation()
+
+		pr.get_unreconciled_entries()
+		invoices = [x.as_dict() for x in pr.get("invoices")]
+		payments = [x.as_dict() for x in pr.get("payments")]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		pr.reconcile()
+
+		# assert outstanding
+		si.reload()
+		self.assertEqual(si.status, "Paid")
+		self.assertEqual(si.outstanding_amount, 0)
+
+		# check PR tool output
+		self.assertEqual(len(pr.get("invoices")), 0)
+		self.assertEqual(len(pr.get("payments")), 0)
+
+	def test_journal_against_journal(self):
+		transaction_date = nowdate()
+		sales = "Sales - _PR"
+		amount = 100
+
+		# debit debtors account to simulate a invoice
+		je1 = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
+		je1.accounts[0].party_type = "Customer"
+		je1.accounts[0].party = self.customer
+		je1.save()
+		je1.submit()
+
+		# credit debtors account to simulate a payment
+		je2 = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
+		je2.accounts[1].party_type = "Customer"
+		je2.accounts[1].party = self.customer
+		je2.save()
+		je2.submit()
+
+		pr = self.create_payment_reconciliation()
+
+		pr.get_unreconciled_entries()
+		invoices = [x.as_dict() for x in pr.get("invoices")]
+		payments = [x.as_dict() for x in pr.get("payments")]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		pr.reconcile()
+
+		self.assertEqual(pr.get("invoices"), [])
+		self.assertEqual(pr.get("payments"), [])
+
+	def test_cr_note_against_invoice(self):
+		transaction_date = nowdate()
+		amount = 100
+
+		si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+
+		cr_note = self.create_sales_invoice(
+			qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
+		)
+		cr_note.is_return = 1
+		cr_note = cr_note.save().submit()
+
+		pr = self.create_payment_reconciliation()
+
+		pr.get_unreconciled_entries()
+		invoices = [x.as_dict() for x in pr.get("invoices")]
+		payments = [x.as_dict() for x in pr.get("payments")]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		pr.reconcile()
+
+		pr.get_unreconciled_entries()
+		# check reconciliation tool output
+		# reconciled invoice and credit note shouldn't show up in selection
+		self.assertEqual(pr.get("invoices"), [])
+		self.assertEqual(pr.get("payments"), [])
+
+		# assert outstanding
+		si.reload()
+		self.assertEqual(si.status, "Paid")
+		self.assertEqual(si.outstanding_amount, 0)
+
+	def test_cr_note_partial_against_invoice(self):
+		transaction_date = nowdate()
+		amount = 100
+		allocated_amount = 80
+
+		si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+
+		cr_note = self.create_sales_invoice(
+			qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
+		)
+		cr_note.is_return = 1
+		cr_note = cr_note.save().submit()
+
+		pr = self.create_payment_reconciliation()
+
+		pr.get_unreconciled_entries()
+		invoices = [x.as_dict() for x in pr.get("invoices")]
+		payments = [x.as_dict() for x in pr.get("payments")]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		pr.allocation[0].allocated_amount = allocated_amount
+		pr.reconcile()
+
+		# assert outstanding
+		si.reload()
+		self.assertEqual(si.status, "Partly Paid")
+		self.assertEqual(si.outstanding_amount, 20)
+
+		pr.get_unreconciled_entries()
+		# check reconciliation tool output
+		self.assertEqual(len(pr.get("invoices")), 1)
+		self.assertEqual(len(pr.get("payments")), 1)
+		self.assertEqual(pr.get("invoices")[0].outstanding_amount, 20)
+		self.assertEqual(pr.get("payments")[0].amount, 20)
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index b0513f1..8146804 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -35,7 +35,13 @@
 			validate_disabled_accounts(gl_map)
 			gl_map = process_gl_map(gl_map, merge_entries)
 			if gl_map and len(gl_map) > 1:
-				create_payment_ledger_entry(gl_map)
+				create_payment_ledger_entry(
+					gl_map,
+					cancel=0,
+					adv_adj=adv_adj,
+					update_outstanding=update_outstanding,
+					from_repost=from_repost,
+				)
 				save_entries(gl_map, adv_adj, update_outstanding, from_repost)
 			# Post GL Map proccess there may no be any GL Entries
 			elif gl_map:
@@ -482,6 +488,9 @@
 
 	if gl_entries:
 		create_payment_ledger_entry(gl_entries, cancel=1)
+		create_payment_ledger_entry(
+			gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding
+		)
 		validate_accounting_period(gl_entries)
 		check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
 		set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 41f3223..8daff9d 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -9,6 +9,8 @@
 import frappe.defaults
 from frappe import _, qb, throw
 from frappe.model.meta import get_field_precision
+from frappe.query_builder import AliasedQuery, Criterion, Table
+from frappe.query_builder.functions import Sum
 from frappe.query_builder.utils import DocType
 from frappe.utils import (
 	cint,
@@ -437,7 +439,8 @@
 		# cancel advance entry
 		doc = frappe.get_doc(voucher_type, voucher_no)
 		frappe.flags.ignore_party_validation = True
-		doc.make_gl_entries(cancel=1, adv_adj=1)
+		gl_map = doc.build_gl_map()
+		create_payment_ledger_entry(gl_map, cancel=1, adv_adj=1)
 
 		for entry in entries:
 			check_if_advance_entry_modified(entry)
@@ -452,7 +455,9 @@
 		doc.save(ignore_permissions=True)
 		# re-submit advance entry
 		doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
-		doc.make_gl_entries(cancel=0, adv_adj=1)
+		gl_map = doc.build_gl_map()
+		create_payment_ledger_entry(gl_map, cancel=0, adv_adj=1)
+
 		frappe.flags.ignore_party_validation = False
 
 		if entry.voucher_type in ("Payment Entry", "Journal Entry"):
@@ -816,7 +821,11 @@
 	return held_invoices
 
 
-def get_outstanding_invoices(party_type, party, account, condition=None, filters=None):
+def get_outstanding_invoices(
+	party_type, party, account, common_filter=None, min_outstanding=None, max_outstanding=None
+):
+
+	ple = qb.DocType("Payment Ledger Entry")
 	outstanding_invoices = []
 	precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
 
@@ -829,76 +838,30 @@
 	else:
 		party_account_type = erpnext.get_party_account_type(party_type)
 
-	if party_account_type == "Receivable":
-		dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
-		payment_dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
-	else:
-		dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
-		payment_dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
-
 	held_invoices = get_held_invoices(party_type, party)
 
-	invoice_list = frappe.db.sql(
-		"""
-		select
-			voucher_no, voucher_type, posting_date, due_date,
-			ifnull(sum({dr_or_cr}), 0) as invoice_amount,
-			account_currency as currency
-		from
-			`tabGL Entry`
-		where
-			party_type = %(party_type)s and party = %(party)s
-			and account = %(account)s and {dr_or_cr} > 0
-			and is_cancelled=0
-			{condition}
-			and ((voucher_type = 'Journal Entry'
-					and (against_voucher = '' or against_voucher is null))
-				or (voucher_type not in ('Journal Entry', 'Payment Entry')))
-		group by voucher_type, voucher_no
-		order by posting_date, name""".format(
-			dr_or_cr=dr_or_cr, condition=condition or ""
-		),
-		{
-			"party_type": party_type,
-			"party": party,
-			"account": account,
-		},
-		as_dict=True,
-	)
+	common_filter = common_filter or []
+	common_filter.append(ple.account_type == party_account_type)
+	common_filter.append(ple.account == account)
+	common_filter.append(ple.party_type == party_type)
+	common_filter.append(ple.party == party)
 
-	payment_entries = frappe.db.sql(
-		"""
-		select against_voucher_type, against_voucher,
-			ifnull(sum({payment_dr_or_cr}), 0) as payment_amount
-		from `tabGL Entry`
-		where party_type = %(party_type)s and party = %(party)s
-			and account = %(account)s
-			and {payment_dr_or_cr} > 0
-			and against_voucher is not null and against_voucher != ''
-			and is_cancelled=0
-		group by against_voucher_type, against_voucher
-	""".format(
-			payment_dr_or_cr=payment_dr_or_cr
-		),
-		{"party_type": party_type, "party": party, "account": account},
-		as_dict=True,
+	ple_query = QueryPaymentLedger()
+	invoice_list = ple_query.get_voucher_outstandings(
+		common_filter=common_filter,
+		min_outstanding=min_outstanding,
+		max_outstanding=max_outstanding,
+		get_invoices=True,
 	)
 
-	pe_map = frappe._dict()
-	for d in payment_entries:
-		pe_map.setdefault((d.against_voucher_type, d.against_voucher), d.payment_amount)
-
 	for d in invoice_list:
-		payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0)
-		outstanding_amount = flt(d.invoice_amount - payment_amount, precision)
+		payment_amount = d.invoice_amount - d.outstanding
+		outstanding_amount = d.outstanding
 		if outstanding_amount > 0.5 / (10**precision):
 			if (
-				filters
-				and filters.get("outstanding_amt_greater_than")
-				and not (
-					outstanding_amount >= filters.get("outstanding_amt_greater_than")
-					and outstanding_amount <= filters.get("outstanding_amt_less_than")
-				)
+				min_outstanding
+				and max_outstanding
+				and not (outstanding_amount >= min_outstanding and outstanding_amount <= max_outstanding)
 			):
 				continue
 
@@ -1389,7 +1352,9 @@
 			frappe.delete_doc("Desktop Icon", icon)
 
 
-def create_payment_ledger_entry(gl_entries, cancel=0):
+def create_payment_ledger_entry(
+	gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0
+):
 	if gl_entries:
 		ple = None
 
@@ -1462,9 +1427,42 @@
 				if cancel:
 					delink_original_entry(ple)
 				ple.flags.ignore_permissions = 1
+				ple.flags.adv_adj = adv_adj
+				ple.flags.from_repost = from_repost
+				ple.flags.update_outstanding = update_outstanding
 				ple.submit()
 
 
+def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
+	ple = frappe.qb.DocType("Payment Ledger Entry")
+	vouchers = [frappe._dict({"voucher_type": voucher_type, "voucher_no": voucher_no})]
+	common_filter = []
+	if account:
+		common_filter.append(ple.account == account)
+
+	if party_type:
+		common_filter.append(ple.party_type == party_type)
+
+	if party:
+		common_filter.append(ple.party == party)
+
+	ple_query = QueryPaymentLedger()
+
+	# on cancellation outstanding can be an empty list
+	voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter)
+	if voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"] and voucher_outstanding:
+		outstanding = voucher_outstanding[0]
+		ref_doc = frappe.get_doc(voucher_type, voucher_no)
+
+		# Didn't use db_set for optimisation purpose
+		ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"]
+		frappe.db.set_value(
+			voucher_type, voucher_no, "outstanding_amount", outstanding["outstanding_in_account_currency"]
+		)
+
+		ref_doc.set_status(update=True)
+
+
 def delink_original_entry(pl_entry):
 	if pl_entry:
 		ple = qb.DocType("Payment Ledger Entry")
@@ -1486,3 +1484,196 @@
 			)
 		)
 		query.run()
+
+
+class QueryPaymentLedger(object):
+	"""
+	Helper Class for Querying Payment Ledger Entry
+	"""
+
+	def __init__(self):
+		self.ple = qb.DocType("Payment Ledger Entry")
+
+		# query result
+		self.voucher_outstandings = []
+
+		# query filters
+		self.vouchers = []
+		self.common_filter = []
+		self.min_outstanding = None
+		self.max_outstanding = None
+
+	def reset(self):
+		# clear filters
+		self.vouchers.clear()
+		self.common_filter.clear()
+		self.min_outstanding = self.max_outstanding = None
+
+		# clear result
+		self.voucher_outstandings.clear()
+
+	def query_for_outstanding(self):
+		"""
+		Database query to fetch voucher amount and voucher outstanding using Common Table Expression
+		"""
+
+		ple = self.ple
+
+		filter_on_voucher_no = []
+		filter_on_against_voucher_no = []
+		if self.vouchers:
+			voucher_types = set([x.voucher_type for x in self.vouchers])
+			voucher_nos = set([x.voucher_no for x in self.vouchers])
+
+			filter_on_voucher_no.append(ple.voucher_type.isin(voucher_types))
+			filter_on_voucher_no.append(ple.voucher_no.isin(voucher_nos))
+
+			filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types))
+			filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos))
+
+		# build outstanding amount filter
+		filter_on_outstanding_amount = []
+		if self.min_outstanding:
+			if self.min_outstanding > 0:
+				filter_on_outstanding_amount.append(
+					Table("outstanding").amount_in_account_currency >= self.min_outstanding
+				)
+			else:
+				filter_on_outstanding_amount.append(
+					Table("outstanding").amount_in_account_currency <= self.min_outstanding
+				)
+		if self.max_outstanding:
+			if self.max_outstanding > 0:
+				filter_on_outstanding_amount.append(
+					Table("outstanding").amount_in_account_currency <= self.max_outstanding
+				)
+			else:
+				filter_on_outstanding_amount.append(
+					Table("outstanding").amount_in_account_currency >= self.max_outstanding
+				)
+
+		# build query for voucher amount
+		query_voucher_amount = (
+			qb.from_(ple)
+			.select(
+				ple.account,
+				ple.voucher_type,
+				ple.voucher_no,
+				ple.party_type,
+				ple.party,
+				ple.posting_date,
+				ple.due_date,
+				ple.account_currency.as_("currency"),
+				Sum(ple.amount).as_("amount"),
+				Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
+			)
+			.where(ple.delinked == 0)
+			.where(Criterion.all(filter_on_voucher_no))
+			.where(Criterion.all(self.common_filter))
+			.groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party)
+		)
+
+		# build query for voucher outstanding
+		query_voucher_outstanding = (
+			qb.from_(ple)
+			.select(
+				ple.account,
+				ple.against_voucher_type.as_("voucher_type"),
+				ple.against_voucher_no.as_("voucher_no"),
+				ple.party_type,
+				ple.party,
+				ple.posting_date,
+				ple.due_date,
+				ple.account_currency.as_("currency"),
+				Sum(ple.amount).as_("amount"),
+				Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
+			)
+			.where(ple.delinked == 0)
+			.where(Criterion.all(filter_on_against_voucher_no))
+			.where(Criterion.all(self.common_filter))
+			.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
+		)
+
+		# build CTE for combining voucher amount and outstanding
+		self.cte_query_voucher_amount_and_outstanding = (
+			qb.with_(query_voucher_amount, "vouchers")
+			.with_(query_voucher_outstanding, "outstanding")
+			.from_(AliasedQuery("vouchers"))
+			.left_join(AliasedQuery("outstanding"))
+			.on(
+				(AliasedQuery("vouchers").account == AliasedQuery("outstanding").account)
+				& (AliasedQuery("vouchers").voucher_type == AliasedQuery("outstanding").voucher_type)
+				& (AliasedQuery("vouchers").voucher_no == AliasedQuery("outstanding").voucher_no)
+				& (AliasedQuery("vouchers").party_type == AliasedQuery("outstanding").party_type)
+				& (AliasedQuery("vouchers").party == AliasedQuery("outstanding").party)
+			)
+			.select(
+				Table("vouchers").account,
+				Table("vouchers").voucher_type,
+				Table("vouchers").voucher_no,
+				Table("vouchers").party_type,
+				Table("vouchers").party,
+				Table("vouchers").posting_date,
+				Table("vouchers").amount.as_("invoice_amount"),
+				Table("vouchers").amount_in_account_currency.as_("invoice_amount_in_account_currency"),
+				Table("outstanding").amount.as_("outstanding"),
+				Table("outstanding").amount_in_account_currency.as_("outstanding_in_account_currency"),
+				(Table("vouchers").amount - Table("outstanding").amount).as_("paid_amount"),
+				(
+					Table("vouchers").amount_in_account_currency - Table("outstanding").amount_in_account_currency
+				).as_("paid_amount_in_account_currency"),
+				Table("vouchers").due_date,
+				Table("vouchers").currency,
+			)
+			.where(Criterion.all(filter_on_outstanding_amount))
+		)
+
+		# build CTE filter
+		# only fetch invoices
+		if self.get_invoices:
+			self.cte_query_voucher_amount_and_outstanding = (
+				self.cte_query_voucher_amount_and_outstanding.having(
+					qb.Field("outstanding_in_account_currency") > 0
+				)
+			)
+		# only fetch payments
+		elif self.get_payments:
+			self.cte_query_voucher_amount_and_outstanding = (
+				self.cte_query_voucher_amount_and_outstanding.having(
+					qb.Field("outstanding_in_account_currency") < 0
+				)
+			)
+
+		# execute SQL
+		self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True)
+
+	def get_voucher_outstandings(
+		self,
+		vouchers=None,
+		common_filter=None,
+		min_outstanding=None,
+		max_outstanding=None,
+		get_payments=False,
+		get_invoices=False,
+	):
+		"""
+		Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
+
+		vouchers - dict of vouchers to get
+		common_filter - array of criterions
+		min_outstanding - filter on minimum total outstanding amount
+		max_outstanding - filter on maximum total  outstanding amount
+		get_invoices - only fetch vouchers(ledger entries with +ve outstanding)
+		get_payments - only fetch payments(ledger entries with -ve outstanding)
+		"""
+
+		self.reset()
+		self.vouchers = vouchers
+		self.common_filter = common_filter or []
+		self.min_outstanding = min_outstanding
+		self.max_outstanding = max_outstanding
+		self.get_payments = get_payments
+		self.get_invoices = get_invoices
+		self.query_for_outstanding()
+
+		return self.voucher_outstandings