Merge branch 'develop' into subcontracting
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index d28c3a8..1451189 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -94,7 +94,7 @@
 
 		unlink_ref_doc_from_payment_entries(self)
 		unlink_ref_doc_from_salary_slip(self.name)
-		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
+		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
 		self.make_gl_entries(1)
 		self.update_advance_paid()
 		self.update_expense_claim()
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index a3a7be2..a10a810 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -95,7 +95,7 @@
 		self.set_status()
 
 	def on_cancel(self):
-		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
+		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
 		self.make_gl_entries(cancel=1)
 		self.update_expense_claim()
 		self.update_outstanding_amounts()
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/__init__.py b/erpnext/accounts/doctype/payment_ledger_entry/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/__init__.py
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js
new file mode 100644
index 0000000..5a7be8e
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Payment Ledger Entry', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
new file mode 100644
index 0000000..d961076
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
@@ -0,0 +1,180 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:PLE-{YY}-{MM}-{######}",
+ "creation": "2022-05-09 19:35:03.334361",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "posting_date",
+  "company",
+  "account_type",
+  "account",
+  "party_type",
+  "party",
+  "due_date",
+  "cost_center",
+  "finance_book",
+  "voucher_type",
+  "voucher_no",
+  "against_voucher_type",
+  "against_voucher_no",
+  "amount",
+  "account_currency",
+  "amount_in_account_currency",
+  "delinked"
+ ],
+ "fields": [
+  {
+   "fieldname": "posting_date",
+   "fieldtype": "Date",
+   "label": "Posting Date"
+  },
+  {
+   "fieldname": "account_type",
+   "fieldtype": "Select",
+   "label": "Account Type",
+   "options": "Receivable\nPayable"
+  },
+  {
+   "fieldname": "account",
+   "fieldtype": "Link",
+   "label": "Account",
+   "options": "Account"
+  },
+  {
+   "fieldname": "party_type",
+   "fieldtype": "Link",
+   "label": "Party Type",
+   "options": "DocType"
+  },
+  {
+   "fieldname": "party",
+   "fieldtype": "Dynamic Link",
+   "label": "Party",
+   "options": "party_type"
+  },
+  {
+   "fieldname": "voucher_type",
+   "fieldtype": "Link",
+   "in_standard_filter": 1,
+   "label": "Voucher Type",
+   "options": "DocType"
+  },
+  {
+   "fieldname": "voucher_no",
+   "fieldtype": "Dynamic Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Voucher No",
+   "options": "voucher_type"
+  },
+  {
+   "fieldname": "against_voucher_type",
+   "fieldtype": "Link",
+   "in_standard_filter": 1,
+   "label": "Against Voucher Type",
+   "options": "DocType"
+  },
+  {
+   "fieldname": "against_voucher_no",
+   "fieldtype": "Dynamic Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Against Voucher No",
+   "options": "against_voucher_type"
+  },
+  {
+   "fieldname": "amount",
+   "fieldtype": "Currency",
+   "in_list_view": 1,
+   "label": "Amount",
+   "options": "Company:company:default_currency"
+  },
+  {
+   "fieldname": "account_currency",
+   "fieldtype": "Link",
+   "label": "Currency",
+   "options": "Currency"
+  },
+  {
+   "fieldname": "amount_in_account_currency",
+   "fieldtype": "Currency",
+   "label": "Amount in Account Currency",
+   "options": "account_currency"
+  },
+  {
+   "default": "0",
+   "fieldname": "delinked",
+   "fieldtype": "Check",
+   "in_list_view": 1,
+   "label": "DeLinked"
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company"
+  },
+  {
+   "fieldname": "cost_center",
+   "fieldtype": "Link",
+   "label": "Cost Center",
+   "options": "Cost Center"
+  },
+  {
+   "fieldname": "due_date",
+   "fieldtype": "Date",
+   "label": "Due Date"
+  },
+  {
+   "fieldname": "finance_book",
+   "fieldtype": "Link",
+   "label": "Finance Book",
+   "options": "Finance Book"
+  }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2022-05-19 18:04:44.609115",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Ledger Entry",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts User",
+   "share": 1
+  },
+  {
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts Manager",
+   "share": 1
+  },
+  {
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Auditor",
+   "share": 1
+  }
+ ],
+ "search_fields": "voucher_no, against_voucher_no",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py
new file mode 100644
index 0000000..43e19f4
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+
+class PaymentLedgerEntry(Document):
+	def validate_account(self):
+		valid_account = frappe.db.get_list(
+			"Account",
+			"name",
+			filters={"name": self.account, "account_type": self.account_type, "company": self.company},
+			ignore_permissions=True,
+		)
+		if not valid_account:
+			frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
+
+	def validate(self):
+		self.validate_account()
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
new file mode 100644
index 0000000..a71b19e
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
@@ -0,0 +1,408 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe import qb
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import nowdate
+
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+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.stock.doctype.item.test_item import create_item
+
+
+class TestPaymentLedgerEntry(FrappeTestCase):
+	def setUp(self):
+		self.ple = qb.DocType("Payment Ledger Entry")
+		self.create_company()
+		self.create_item()
+		self.create_customer()
+		self.clear_old_entries()
+
+	def tearDown(self):
+		frappe.db.rollback()
+
+	def create_company(self):
+		company_name = "_Test Payment Ledger"
+		company = None
+		if frappe.db.exists("Company", company_name):
+			company = frappe.get_doc("Company", company_name)
+		else:
+			company = frappe.get_doc(
+				{
+					"doctype": "Company",
+					"company_name": company_name,
+					"country": "India",
+					"default_currency": "INR",
+					"create_chart_of_accounts_based_on": "Standard Template",
+					"chart_of_accounts": "Standard",
+				}
+			)
+			company = company.save()
+
+		self.company = company.name
+		self.cost_center = company.cost_center
+		self.warehouse = "All Warehouses - _PL"
+		self.income_account = "Sales - _PL"
+		self.expense_account = "Cost of Goods Sold - _PL"
+		self.debit_to = "Debtors - _PL"
+		self.creditors = "Creditors - _PL"
+
+		# create bank account
+		if frappe.db.exists("Account", "HDFC - _PL"):
+			self.bank = "HDFC - _PL"
+		else:
+			bank_acc = frappe.get_doc(
+				{
+					"doctype": "Account",
+					"account_name": "HDFC",
+					"parent_account": "Bank Accounts - _PL",
+					"company": self.company,
+				}
+			)
+			bank_acc.save()
+			self.bank = bank_acc.name
+
+	def create_item(self):
+		item_name = "_Test PL Item"
+		item = create_item(
+			item_code=item_name, is_stock_item=0, company=self.company, warehouse=self.warehouse
+		)
+		self.item = item if isinstance(item, str) else item.item_code
+
+	def create_customer(self):
+		name = "_Test PL Customer"
+		if frappe.db.exists("Customer", name):
+			self.customer = name
+		else:
+			customer = frappe.new_doc("Customer")
+			customer.customer_name = name
+			customer.type = "Individual"
+			customer.save()
+			self.customer = customer.name
+
+	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 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()
+
+	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_payment_against_invoice(self):
+		transaction_date = nowdate()
+		amount = 100
+		ple = self.ple
+
+		# full payment using PE
+		si1 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+		pe1 = get_payment_entry(si1.doctype, si1.name).save().submit()
+
+		pl_entries = (
+			qb.from_(ple)
+			.select(
+				ple.voucher_type,
+				ple.voucher_no,
+				ple.against_voucher_type,
+				ple.against_voucher_no,
+				ple.amount,
+				ple.delinked,
+			)
+			.where((ple.against_voucher_type == si1.doctype) & (ple.against_voucher_no == si1.name))
+			.orderby(ple.creation)
+			.run(as_dict=True)
+		)
+
+		expected_values = [
+			{
+				"voucher_type": si1.doctype,
+				"voucher_no": si1.name,
+				"against_voucher_type": si1.doctype,
+				"against_voucher_no": si1.name,
+				"amount": amount,
+				"delinked": 0,
+			},
+			{
+				"voucher_type": pe1.doctype,
+				"voucher_no": pe1.name,
+				"against_voucher_type": si1.doctype,
+				"against_voucher_no": si1.name,
+				"amount": -amount,
+				"delinked": 0,
+			},
+		]
+		self.assertEqual(pl_entries[0], expected_values[0])
+		self.assertEqual(pl_entries[1], expected_values[1])
+
+	def test_partial_payment_against_invoice(self):
+		ple = self.ple
+		transaction_date = nowdate()
+		amount = 100
+
+		# partial payment of invoice using PE
+		si2 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+		pe2 = get_payment_entry(si2.doctype, si2.name)
+		pe2.get("references")[0].allocated_amount = 50
+		pe2.get("references")[0].outstanding_amount = 50
+		pe2 = pe2.save().submit()
+
+		pl_entries = (
+			qb.from_(ple)
+			.select(
+				ple.voucher_type,
+				ple.voucher_no,
+				ple.against_voucher_type,
+				ple.against_voucher_no,
+				ple.amount,
+				ple.delinked,
+			)
+			.where((ple.against_voucher_type == si2.doctype) & (ple.against_voucher_no == si2.name))
+			.orderby(ple.creation)
+			.run(as_dict=True)
+		)
+
+		expected_values = [
+			{
+				"voucher_type": si2.doctype,
+				"voucher_no": si2.name,
+				"against_voucher_type": si2.doctype,
+				"against_voucher_no": si2.name,
+				"amount": amount,
+				"delinked": 0,
+			},
+			{
+				"voucher_type": pe2.doctype,
+				"voucher_no": pe2.name,
+				"against_voucher_type": si2.doctype,
+				"against_voucher_no": si2.name,
+				"amount": -50,
+				"delinked": 0,
+			},
+		]
+		self.assertEqual(pl_entries[0], expected_values[0])
+		self.assertEqual(pl_entries[1], expected_values[1])
+
+	def test_cr_note_against_invoice(self):
+		ple = self.ple
+		transaction_date = nowdate()
+		amount = 100
+
+		# reconcile against return invoice
+		si3 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+		cr_note1 = self.create_sales_invoice(
+			qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
+		)
+		cr_note1.is_return = 1
+		cr_note1.return_against = si3.name
+		cr_note1 = cr_note1.save().submit()
+
+		pl_entries = (
+			qb.from_(ple)
+			.select(
+				ple.voucher_type,
+				ple.voucher_no,
+				ple.against_voucher_type,
+				ple.against_voucher_no,
+				ple.amount,
+				ple.delinked,
+			)
+			.where((ple.against_voucher_type == si3.doctype) & (ple.against_voucher_no == si3.name))
+			.orderby(ple.creation)
+			.run(as_dict=True)
+		)
+
+		expected_values = [
+			{
+				"voucher_type": si3.doctype,
+				"voucher_no": si3.name,
+				"against_voucher_type": si3.doctype,
+				"against_voucher_no": si3.name,
+				"amount": amount,
+				"delinked": 0,
+			},
+			{
+				"voucher_type": cr_note1.doctype,
+				"voucher_no": cr_note1.name,
+				"against_voucher_type": si3.doctype,
+				"against_voucher_no": si3.name,
+				"amount": -amount,
+				"delinked": 0,
+			},
+		]
+		self.assertEqual(pl_entries[0], expected_values[0])
+		self.assertEqual(pl_entries[1], expected_values[1])
+
+	def test_je_against_inv_and_note(self):
+		ple = self.ple
+		transaction_date = nowdate()
+		amount = 100
+
+		# reconcile against return invoice using JE
+		si4 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+		cr_note2 = self.create_sales_invoice(
+			qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
+		)
+		cr_note2.is_return = 1
+		cr_note2 = cr_note2.save().submit()
+		je1 = self.create_journal_entry(
+			self.debit_to, self.debit_to, amount, posting_date=transaction_date
+		)
+		je1.get("accounts")[0].party_type = je1.get("accounts")[1].party_type = "Customer"
+		je1.get("accounts")[0].party = je1.get("accounts")[1].party = self.customer
+		je1.get("accounts")[0].reference_type = cr_note2.doctype
+		je1.get("accounts")[0].reference_name = cr_note2.name
+		je1.get("accounts")[1].reference_type = si4.doctype
+		je1.get("accounts")[1].reference_name = si4.name
+		je1 = je1.save().submit()
+
+		pl_entries_for_invoice = (
+			qb.from_(ple)
+			.select(
+				ple.voucher_type,
+				ple.voucher_no,
+				ple.against_voucher_type,
+				ple.against_voucher_no,
+				ple.amount,
+				ple.delinked,
+			)
+			.where((ple.against_voucher_type == si4.doctype) & (ple.against_voucher_no == si4.name))
+			.orderby(ple.creation)
+			.run(as_dict=True)
+		)
+
+		expected_values = [
+			{
+				"voucher_type": si4.doctype,
+				"voucher_no": si4.name,
+				"against_voucher_type": si4.doctype,
+				"against_voucher_no": si4.name,
+				"amount": amount,
+				"delinked": 0,
+			},
+			{
+				"voucher_type": je1.doctype,
+				"voucher_no": je1.name,
+				"against_voucher_type": si4.doctype,
+				"against_voucher_no": si4.name,
+				"amount": -amount,
+				"delinked": 0,
+			},
+		]
+		self.assertEqual(pl_entries_for_invoice[0], expected_values[0])
+		self.assertEqual(pl_entries_for_invoice[1], expected_values[1])
+
+		pl_entries_for_crnote = (
+			qb.from_(ple)
+			.select(
+				ple.voucher_type,
+				ple.voucher_no,
+				ple.against_voucher_type,
+				ple.against_voucher_no,
+				ple.amount,
+				ple.delinked,
+			)
+			.where(
+				(ple.against_voucher_type == cr_note2.doctype) & (ple.against_voucher_no == cr_note2.name)
+			)
+			.orderby(ple.creation)
+			.run(as_dict=True)
+		)
+
+		expected_values = [
+			{
+				"voucher_type": cr_note2.doctype,
+				"voucher_no": cr_note2.name,
+				"against_voucher_type": cr_note2.doctype,
+				"against_voucher_no": cr_note2.name,
+				"amount": -amount,
+				"delinked": 0,
+			},
+			{
+				"voucher_type": je1.doctype,
+				"voucher_no": je1.name,
+				"against_voucher_type": cr_note2.doctype,
+				"against_voucher_no": cr_note2.name,
+				"amount": amount,
+				"delinked": 0,
+			},
+		]
+		self.assertEqual(pl_entries_for_crnote[0], expected_values[0])
+		self.assertEqual(pl_entries_for_crnote[1], expected_values[1])
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 94246e1..9649f80 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -96,6 +96,7 @@
 			)
 
 	def on_cancel(self):
+		self.ignore_linked_doctypes = "Payment Ledger Entry"
 		# run on cancel method of selling controller
 		super(SalesInvoice, self).on_cancel()
 		if not self.is_return and self.loyalty_program:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 08de5b2..0d086de 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1416,7 +1416,12 @@
 		frappe.db.set(self, "status", "Cancelled")
 
 		unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
-		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
+		self.ignore_linked_doctypes = (
+			"GL Entry",
+			"Stock Ledger Entry",
+			"Repost Item Valuation",
+			"Payment Ledger Entry",
+		)
 		self.update_advance_tax_references(cancel=1)
 
 	def update_project(self):
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index f0880c1..a580d45 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -396,7 +396,12 @@
 		unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
 
 		self.unlink_sales_invoice_from_timesheets()
-		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
+		self.ignore_linked_doctypes = (
+			"GL Entry",
+			"Stock Ledger Entry",
+			"Repost Item Valuation",
+			"Payment Ledger Entry",
+		)
 
 	def update_status_updater_args(self):
 		if cint(self.update_stock):
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 1598d91..b0513f1 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -14,6 +14,7 @@
 	get_accounting_dimensions,
 )
 from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
+from erpnext.accounts.utils import create_payment_ledger_entry
 
 
 class ClosedAccountingPeriod(frappe.ValidationError):
@@ -34,6 +35,7 @@
 			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)
 				save_entries(gl_map, adv_adj, update_outstanding, from_repost)
 			# Post GL Map proccess there may no be any GL Entries
 			elif gl_map:
@@ -479,6 +481,7 @@
 		).run(as_dict=1)
 
 	if gl_entries:
+		create_payment_ledger_entry(gl_entries, cancel=1)
 		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 405922e..1869cc7 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -7,7 +7,7 @@
 
 import frappe
 import frappe.defaults
-from frappe import _, throw
+from frappe import _, qb, throw
 from frappe.model.meta import get_field_precision
 from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
 
@@ -15,6 +15,7 @@
 
 # imported to enable erpnext.accounts.utils.get_account_currency
 from erpnext.accounts.doctype.account.account import get_account_currency  # noqa
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
 from erpnext.stock import get_warehouse_account_map
 from erpnext.stock.utils import get_stock_value_on
 
@@ -1345,3 +1346,102 @@
 	if icons:
 		for icon in icons:
 			frappe.delete_doc("Desktop Icon", icon)
+
+
+def create_payment_ledger_entry(gl_entries, cancel=0):
+	if gl_entries:
+		ple = None
+
+		# companies
+		account = qb.DocType("Account")
+		companies = list(set([x.company for x in gl_entries]))
+
+		# receivable/payable account
+		accounts_with_types = (
+			qb.from_(account)
+			.select(account.name, account.account_type)
+			.where(
+				(account.account_type.isin(["Receivable", "Payable"]) & (account.company.isin(companies)))
+			)
+			.run(as_dict=True)
+		)
+		receivable_or_payable_accounts = [y.name for y in accounts_with_types]
+
+		def get_account_type(account):
+			for entry in accounts_with_types:
+				if entry.name == account:
+					return entry.account_type
+
+		dr_or_cr = 0
+		account_type = None
+		for gle in gl_entries:
+			if gle.account in receivable_or_payable_accounts:
+				account_type = get_account_type(gle.account)
+				if account_type == "Receivable":
+					dr_or_cr = gle.debit - gle.credit
+					dr_or_cr_account_currency = gle.debit_in_account_currency - gle.credit_in_account_currency
+				elif account_type == "Payable":
+					dr_or_cr = gle.credit - gle.debit
+					dr_or_cr_account_currency = gle.credit_in_account_currency - gle.debit_in_account_currency
+
+				if cancel:
+					dr_or_cr *= -1
+					dr_or_cr_account_currency *= -1
+
+				ple = frappe.get_doc(
+					{
+						"doctype": "Payment Ledger Entry",
+						"posting_date": gle.posting_date,
+						"company": gle.company,
+						"account_type": account_type,
+						"account": gle.account,
+						"party_type": gle.party_type,
+						"party": gle.party,
+						"cost_center": gle.cost_center,
+						"finance_book": gle.finance_book,
+						"due_date": gle.due_date,
+						"voucher_type": gle.voucher_type,
+						"voucher_no": gle.voucher_no,
+						"against_voucher_type": gle.against_voucher_type
+						if gle.against_voucher_type
+						else gle.voucher_type,
+						"against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no,
+						"currency": gle.currency,
+						"amount": dr_or_cr,
+						"amount_in_account_currency": dr_or_cr_account_currency,
+						"delinked": True if cancel else False,
+					}
+				)
+
+				dimensions_and_defaults = get_dimensions()
+				if dimensions_and_defaults:
+					for dimension in dimensions_and_defaults[0]:
+						ple.set(dimension.fieldname, gle.get(dimension.fieldname))
+
+				if cancel:
+					delink_original_entry(ple)
+				ple.flags.ignore_permissions = 1
+				ple.submit()
+
+
+def delink_original_entry(pl_entry):
+	if pl_entry:
+		ple = qb.DocType("Payment Ledger Entry")
+		query = (
+			qb.update(ple)
+			.set(ple.delinked, True)
+			.set(ple.modified, now())
+			.set(ple.modified_by, frappe.session.user)
+			.where(
+				(ple.company == pl_entry.company)
+				& (ple.account_type == pl_entry.account_type)
+				& (ple.account == pl_entry.account)
+				& (ple.party_type == pl_entry.party_type)
+				& (ple.party == pl_entry.party)
+				& (ple.voucher_type == pl_entry.voucher_type)
+				& (ple.voucher_no == pl_entry.voucher_no)
+				& (ple.against_voucher_type == pl_entry.against_voucher_type)
+				& (ple.against_voucher_no == pl_entry.against_voucher_no)
+			)
+		)
+		query.run()
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 234bec1..e0cc43f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -328,6 +328,7 @@
 		update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
 
 	def on_cancel(self):
+		self.ignore_linked_doctypes = "Payment Ledger Entry"
 		super(PurchaseOrder, self).on_cancel()
 
 		if self.is_against_so():
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 813ac17..1c4bbbc 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -487,6 +487,7 @@
 
 accounting_dimension_doctypes = [
 	"GL Entry",
+	"Payment Ledger Entry",
 	"Sales Invoice",
 	"Purchase Invoice",
 	"Payment Entry",
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index 89d86c1..589763c 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -105,7 +105,7 @@
 
 	def on_cancel(self):
 		self.update_task_and_project()
-		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
+		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
 		if self.payable_account:
 			self.make_gl_entries(cancel=True)
 
diff --git a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
new file mode 100644
index 0000000..c2267aa
--- /dev/null
+++ b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
@@ -0,0 +1,38 @@
+import frappe
+from frappe import qb
+
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+	get_dimensions,
+	make_dimension_in_accounting_doctypes,
+)
+from erpnext.accounts.utils import create_payment_ledger_entry
+
+
+def create_accounting_dimension_fields():
+	dimensions_and_defaults = get_dimensions()
+	if dimensions_and_defaults:
+		for dimension in dimensions_and_defaults[0]:
+			make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
+
+
+def execute():
+	# create accounting dimension fields in Payment Ledger
+	create_accounting_dimension_fields()
+
+	gl = qb.DocType("GL Entry")
+	accounts = frappe.db.get_list(
+		"Account", "name", filters={"account_type": ["in", ["Receivable", "Payable"]]}, as_list=True
+	)
+	gl_entries = []
+	if accounts:
+		# get all gl entries on receivable/payable accounts
+		gl_entries = (
+			qb.from_(gl)
+			.select("*")
+			.where(gl.account.isin(accounts))
+			.where(gl.is_cancelled == 0)
+			.run(as_dict=True)
+		)
+		if gl_entries:
+			# create payment ledger entries for the accounts receivable/payable
+			create_payment_ledger_entry(gl_entries, 0)
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index b463213..7522e92 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -232,7 +232,7 @@
 			update_coupon_code_count(self.coupon_code, "used")
 
 	def on_cancel(self):
-		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
+		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
 		super(SalesOrder, self).on_cancel()
 
 		# Cannot cancel closed SO