Merge branch 'develop' into revert-33699-tds_report_percentage
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index a354d7a..2996836 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -40,6 +40,8 @@
   "show_payment_schedule_in_print",
   "currency_exchange_section",
   "allow_stale",
+  "section_break_jpd0",
+  "auto_reconcile_payments",
   "stale_days",
   "invoicing_settings_tab",
   "accounts_transactions_settings_section",
@@ -59,7 +61,6 @@
   "acc_frozen_upto",
   "column_break_25",
   "frozen_accounts_modifier",
-  "report_settings_sb",
   "tab_break_dpet",
   "show_balance_in_coa"
  ],
@@ -173,11 +174,6 @@
    "label": "Stale Days"
   },
   {
-   "fieldname": "report_settings_sb",
-   "fieldtype": "Section Break",
-   "label": "Report Settings"
-  },
-  {
    "default": "0",
    "description": "Only select this if you have set up the Cash Flow Mapper documents",
    "fieldname": "use_custom_cash_flow",
@@ -383,6 +379,17 @@
    "fieldname": "merge_similar_account_heads",
    "fieldtype": "Check",
    "label": "Merge Similar Account Heads"
+  },
+  {
+   "fieldname": "section_break_jpd0",
+   "fieldtype": "Section Break",
+   "label": "Payment Reconciliations"
+  },
+  {
+   "default": "0",
+   "fieldname": "auto_reconcile_payments",
+   "fieldtype": "Check",
+   "label": "Auto Reconcile Payments"
   }
  ],
  "icon": "icon-cog",
@@ -390,7 +397,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2023-04-17 11:45:42.049247",
+ "modified": "2023-04-21 13:11:37.130743",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index caffac5..08d38dd 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -82,6 +82,32 @@
 			this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
 			this.frm.change_custom_button_type('Allocate', null, 'default');
 		}
+
+		// check for any running reconciliation jobs
+		if (this.frm.doc.receivable_payable_account) {
+			frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments").then((enabled) => {
+ 				if(enabled) {
+					this.frm.call({
+						'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
+						"args": {
+							for_filter: {
+								company: this.frm.doc.company,
+								party_type: this.frm.doc.party_type,
+								party: this.frm.doc.party,
+								receivable_payable_account: this.frm.doc.receivable_payable_account
+							}
+						}
+					}).then(r => {
+						if (r.message) {
+							let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
+							let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
+							this.frm.dashboard.add_comment(msg, "yellow");
+						}
+					});
+				}
+			});
+		}
+
 	}
 
 	company() {
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index d8082d0..cc2b942 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -7,9 +7,12 @@
 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
+from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
 
 import erpnext
+from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
+	is_any_doc_running,
+)
 from erpnext.accounts.utils import (
 	QueryPaymentLedger,
 	get_outstanding_invoices,
@@ -304,9 +307,7 @@
 			}
 		)
 
-	@frappe.whitelist()
-	def reconcile(self):
-		self.validate_allocation()
+	def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
 		dr_or_cr = (
 			"credit_in_account_currency"
 			if erpnext.get_party_account_type(self.party_type) == "Receivable"
@@ -330,12 +331,35 @@
 					self.make_difference_entry(payment_details)
 
 		if entry_list:
-			reconcile_against_document(entry_list)
+			reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
 
 		if dr_or_cr_notes:
 			reconcile_dr_cr_note(dr_or_cr_notes, self.company)
 
+	@frappe.whitelist()
+	def reconcile(self):
+		if frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
+			running_doc = is_any_doc_running(
+				dict(
+					company=self.company,
+					party_type=self.party_type,
+					party=self.party,
+					receivable_payable_account=self.receivable_payable_account,
+				)
+			)
+
+			if running_doc:
+				frappe.throw(
+					_("A Reconciliation Job {0} is running for the same filters. Cannot reconcile now").format(
+						get_link_to_form("Auto Reconcile", running_doc)
+					)
+				)
+				return
+
+		self.validate_allocation()
+		self.reconcile_allocations()
 		msgprint(_("Successfully Reconciled"))
+
 		self.get_unreconciled_entries()
 
 	def make_difference_entry(self, row):
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/__init__.py b/erpnext/accounts/doctype/process_payment_reconciliation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/__init__.py
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js
new file mode 100644
index 0000000..dd601bf
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js
@@ -0,0 +1,130 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Process Payment Reconciliation", {
+	onload: function(frm) {
+		// set queries
+		frm.set_query("party_type", function() {
+			return {
+				"filters": {
+					"name": ["in", Object.keys(frappe.boot.party_account_types)],
+				}
+			}
+		});
+		frm.set_query('receivable_payable_account',  function(doc) {
+			return {
+				filters: {
+					"company": doc.company,
+					"is_group": 0,
+					"account_type": frappe.boot.party_account_types[doc.party_type]
+				}
+			};
+		});
+		frm.set_query('cost_center', function(doc) {
+			return {
+				filters: {
+					"company": doc.company,
+					"is_group": 0,
+				}
+			};
+		});
+		frm.set_query('bank_cash_account', function(doc) {
+			return {
+				filters:[
+					['Account', 'company', '=', doc.company],
+					['Account', 'is_group', '=', 0],
+					['Account', 'account_type', 'in', ['Bank', 'Cash']]
+				]
+			};
+		});
+
+	},
+	refresh: function(frm) {
+		if (frm.doc.docstatus==1 && ['Queued', 'Paused'].find(x => x == frm.doc.status)) {
+			let execute_btn = __("Start / Resume")
+
+			frm.add_custom_button(execute_btn, () => {
+				frm.call({
+					method: 'erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_job_for_doc',
+					args: {
+						docname: frm.doc.name
+					}
+				}).then(r => {
+					if(!r.exc) {
+						frappe.show_alert(__("Job Started"));
+						frm.reload_doc();
+					}
+				});
+			});
+		}
+		if (frm.doc.docstatus==1 && ['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
+			frm.call({
+				'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.get_reconciled_count",
+				args: {
+					"docname": frm.docname,
+				}
+			}).then(r => {
+				if (r.message) {
+					let progress = 0;
+					let description = "";
+
+					if (r.message.processed) {
+						progress = (r.message.processed/r.message.total) * 100;
+						description = r.message.processed + "/" + r.message.total +  " processed";
+					} else if (r.message.total == 0 && frm.doc.status == "Completed") {
+						progress = 100;
+					}
+
+
+					frm.dashboard.add_progress('Reconciliation Progress', progress, description);
+				}
+			})
+		}
+		if (frm.doc.docstatus==1 && frm.doc.status == 'Running') {
+			let execute_btn = __("Pause")
+
+			frm.add_custom_button(execute_btn, () => {
+				frm.call({
+					'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.pause_job_for_doc",
+					args: {
+						"docname": frm.docname,
+					}
+				}).then(r => {
+					if (!r.exc) {
+						frappe.show_alert(__("Job Paused"));
+						frm.reload_doc()
+					}
+				});
+
+			});
+		}
+	},
+	company(frm) {
+		frm.set_value('party', '');
+		frm.set_value('receivable_payable_account', '');
+	},
+	party_type(frm) {
+		frm.set_value('party', '');
+	},
+
+	party(frm) {
+		frm.set_value('receivable_payable_account', '');
+		if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
+			return frappe.call({
+				method: "erpnext.accounts.party.get_party_account",
+				args: {
+					company: frm.doc.company,
+					party_type: frm.doc.party_type,
+					party: frm.doc.party
+				},
+				callback: (r) => {
+					if (!r.exc && r.message) {
+						frm.set_value("receivable_payable_account", r.message);
+					}
+					frm.refresh();
+
+				}
+			});
+		}
+	}
+});
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json
new file mode 100644
index 0000000..8bb7092
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json
@@ -0,0 +1,173 @@
+{
+ "actions": [],
+ "autoname": "format:ACC-PPR-{#####}",
+ "beta": 1,
+ "creation": "2023-03-30 21:28:39.793927",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "company",
+  "party_type",
+  "column_break_io6c",
+  "party",
+  "receivable_payable_account",
+  "filter_section",
+  "from_invoice_date",
+  "to_invoice_date",
+  "column_break_kegk",
+  "from_payment_date",
+  "to_payment_date",
+  "column_break_uj04",
+  "cost_center",
+  "bank_cash_account",
+  "section_break_2n02",
+  "status",
+  "error_log",
+  "section_break_a8yx",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "allow_on_submit": 1,
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "label": "Status",
+   "options": "\nQueued\nRunning\nPaused\nCompleted\nPartially Reconciled\nFailed\nCancelled",
+   "read_only": 1
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Company",
+   "options": "Company",
+   "reqd": 1
+  },
+  {
+   "fieldname": "party_type",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Party Type",
+   "options": "DocType",
+   "reqd": 1
+  },
+  {
+   "fieldname": "column_break_io6c",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "party",
+   "fieldtype": "Dynamic Link",
+   "in_list_view": 1,
+   "label": "Party",
+   "options": "party_type",
+   "reqd": 1
+  },
+  {
+   "fieldname": "receivable_payable_account",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Receivable/Payable Account",
+   "options": "Account",
+   "reqd": 1
+  },
+  {
+   "fieldname": "filter_section",
+   "fieldtype": "Section Break",
+   "label": "Filters"
+  },
+  {
+   "fieldname": "from_invoice_date",
+   "fieldtype": "Date",
+   "label": "From Invoice Date"
+  },
+  {
+   "fieldname": "to_invoice_date",
+   "fieldtype": "Date",
+   "label": "To Invoice Date"
+  },
+  {
+   "fieldname": "column_break_kegk",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "from_payment_date",
+   "fieldtype": "Date",
+   "label": "From Payment Date"
+  },
+  {
+   "fieldname": "to_payment_date",
+   "fieldtype": "Date",
+   "label": "To Payment Date"
+  },
+  {
+   "fieldname": "column_break_uj04",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "cost_center",
+   "fieldtype": "Link",
+   "label": "Cost Center",
+   "options": "Cost Center"
+  },
+  {
+   "fieldname": "bank_cash_account",
+   "fieldtype": "Link",
+   "label": "Bank/Cash Account",
+   "options": "Account"
+  },
+  {
+   "fieldname": "section_break_2n02",
+   "fieldtype": "Section Break",
+   "label": "Status"
+  },
+  {
+   "depends_on": "eval:doc.error_log",
+   "fieldname": "error_log",
+   "fieldtype": "Long Text",
+   "label": "Error Log"
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Process Payment Reconciliation",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "section_break_a8yx",
+   "fieldtype": "Section Break"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-04-21 17:19:30.912953",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Process Payment Reconciliation",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "company"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
new file mode 100644
index 0000000..ecb51ce
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
@@ -0,0 +1,503 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, qb
+from frappe.model.document import Document
+from frappe.utils import get_link_to_form
+from frappe.utils.scheduler import is_scheduler_inactive
+
+
+class ProcessPaymentReconciliation(Document):
+	def validate(self):
+		self.validate_receivable_payable_account()
+		self.validate_bank_cash_account()
+
+	def validate_receivable_payable_account(self):
+		if self.receivable_payable_account:
+			if self.company != frappe.db.get_value("Account", self.receivable_payable_account, "company"):
+				frappe.throw(
+					_("Receivable/Payable Account: {0} doesn't belong to company {1}").format(
+						frappe.bold(self.receivable_payable_account), frappe.bold(self.company)
+					)
+				)
+
+	def validate_bank_cash_account(self):
+		if self.bank_cash_account:
+			if self.company != frappe.db.get_value("Account", self.bank_cash_account, "company"):
+				frappe.throw(
+					_("Bank/Cash Account {0} doesn't belong to company {1}").format(
+						frappe.bold(self.bank_cash_account), frappe.bold(self.company)
+					)
+				)
+
+	def before_save(self):
+		self.status = ""
+		self.error_log = ""
+
+	def on_submit(self):
+		self.db_set("status", "Queued")
+		self.db_set("error_log", None)
+
+	def on_cancel(self):
+		self.db_set("status", "Cancelled")
+		log = frappe.db.get_value(
+			"Process Payment Reconciliation Log", filters={"process_pr": self.name}
+		)
+		if log:
+			frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Cancelled")
+
+
+@frappe.whitelist()
+def get_reconciled_count(docname: str | None = None) -> float:
+	current_status = {}
+	if docname:
+		reconcile_log = frappe.db.get_value(
+			"Process Payment Reconciliation Log", filters={"process_pr": docname}, fieldname="name"
+		)
+		if reconcile_log:
+			res = frappe.get_all(
+				"Process Payment Reconciliation Log",
+				filters={"name": reconcile_log},
+				fields=["reconciled_entries", "total_allocations"],
+				as_list=1,
+			)
+			current_status["processed"], current_status["total"] = res[0]
+
+	return current_status
+
+
+def get_pr_instance(doc: str):
+	process_payment_reconciliation = frappe.get_doc("Process Payment Reconciliation", doc)
+
+	pr = frappe.get_doc("Payment Reconciliation")
+	fields = [
+		"company",
+		"party_type",
+		"party",
+		"receivable_payable_account",
+		"from_invoice_date",
+		"to_invoice_date",
+		"from_payment_date",
+		"to_payment_date",
+	]
+	d = {}
+	for field in fields:
+		d[field] = process_payment_reconciliation.get(field)
+	pr.update(d)
+	pr.invoice_limit = 1000
+	pr.payment_limit = 1000
+	return pr
+
+
+def is_job_running(job_name: str) -> bool:
+	jobs = frappe.db.get_all("RQ Job", filters={"status": ["in", ["started", "queued"]]})
+	for x in jobs:
+		if x.job_name == job_name:
+			return True
+	return False
+
+
+@frappe.whitelist()
+def pause_job_for_doc(docname: str | None = None):
+	if docname:
+		frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
+		log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
+		if log:
+			frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Paused")
+
+
+@frappe.whitelist()
+def trigger_job_for_doc(docname: str | None = None):
+	"""
+	Trigger background job
+	"""
+	if not docname:
+		return
+
+	if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
+		frappe.throw(
+			_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
+				get_link_to_form("Accounts Settings", "Accounts Settings")
+			)
+		)
+
+		return
+
+	if not is_scheduler_inactive():
+		if frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Queued":
+			frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
+			job_name = f"start_processing_{docname}"
+			if not is_job_running(job_name):
+				job = frappe.enqueue(
+					method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
+					queue="long",
+					is_async=True,
+					job_name=job_name,
+					enqueue_after_commit=True,
+					doc=docname,
+				)
+
+		elif frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Paused":
+			frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
+			log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
+			if log:
+				frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Running")
+
+			# Resume tasks for running doc
+			job_name = f"start_processing_{docname}"
+			if not is_job_running(job_name):
+				job = frappe.enqueue(
+					method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
+					queue="long",
+					is_async=True,
+					job_name=job_name,
+					doc=docname,
+				)
+	else:
+		frappe.msgprint(_("Scheduler is Inactive. Can't trigger job now."))
+
+
+def trigger_reconciliation_for_queued_docs():
+	"""
+	Will be called from Cron Job
+	Fetch queued docs and start reconciliation process for each one
+	"""
+	if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
+		frappe.throw(
+			_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
+				get_link_to_form("Accounts Settings", "Accounts Settings")
+			)
+		)
+
+		return
+
+	if not is_scheduler_inactive():
+		# Get all queued documents
+		all_queued = frappe.db.get_all(
+			"Process Payment Reconciliation",
+			filters={"docstatus": 1, "status": "Queued"},
+			order_by="creation desc",
+			as_list=1,
+		)
+
+		docs_to_trigger = []
+		unique_filters = set()
+		queue_size = 5
+
+		fields = ["company", "party_type", "party", "receivable_payable_account"]
+
+		def get_filters_as_tuple(fields, doc):
+			filters = ()
+			for x in fields:
+				filters += tuple(doc.get(x))
+			return filters
+
+		for x in all_queued:
+			doc = frappe.get_doc("Process Payment Reconciliation", x)
+			filters = get_filters_as_tuple(fields, doc)
+			if filters not in unique_filters:
+				unique_filters.add(filters)
+				docs_to_trigger.append(doc.name)
+			if len(docs_to_trigger) == queue_size:
+				break
+
+		# trigger reconcilation process for queue_size unique filters
+		for doc in docs_to_trigger:
+			trigger_job_for_doc(doc)
+
+	else:
+		frappe.msgprint(_("Scheduler is Inactive. Can't trigger jobs now."))
+
+
+def reconcile_based_on_filters(doc: None | str = None) -> None:
+	"""
+	Identify current state of document and execute next tasks in background
+	"""
+	if doc:
+		log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
+		if not log:
+			log = frappe.new_doc("Process Payment Reconciliation Log")
+			log.process_pr = doc
+			log.status = "Running"
+			log = log.save()
+
+			job_name = f"process_{doc}_fetch_and_allocate"
+			if not is_job_running(job_name):
+				job = frappe.enqueue(
+					method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
+					queue="long",
+					timeout="3600",
+					is_async=True,
+					job_name=job_name,
+					enqueue_after_commit=True,
+					doc=doc,
+				)
+		else:
+			res = frappe.get_all(
+				"Process Payment Reconciliation Log",
+				filters={"name": log},
+				fields=["allocated", "reconciled"],
+				as_list=1,
+			)
+			allocated, reconciled = res[0]
+
+			if not allocated:
+				job_name = f"process__{doc}_fetch_and_allocate"
+				if not is_job_running(job_name):
+					job = frappe.enqueue(
+						method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
+						queue="long",
+						timeout="3600",
+						is_async=True,
+						job_name=job_name,
+						enqueue_after_commit=True,
+						doc=doc,
+					)
+			elif not reconciled:
+				allocation = get_next_allocation(log)
+				if allocation:
+					reconcile_job_name = (
+						f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
+					)
+				else:
+					reconcile_job_name = f"process_{doc}_reconcile"
+				if not is_job_running(reconcile_job_name):
+					job = frappe.enqueue(
+						method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
+						queue="long",
+						timeout="3600",
+						is_async=True,
+						job_name=reconcile_job_name,
+						enqueue_after_commit=True,
+						doc=doc,
+					)
+			elif reconciled:
+				frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
+
+
+def get_next_allocation(log: str) -> list:
+	if log:
+		allocations = []
+		next = frappe.db.get_all(
+			"Process Payment Reconciliation Log Allocations",
+			filters={"parent": log, "reconciled": 0},
+			fields=["reference_type", "reference_name"],
+			order_by="idx",
+			limit=1,
+		)
+
+		if next:
+			allocations = frappe.db.get_all(
+				"Process Payment Reconciliation Log Allocations",
+				filters={
+					"parent": log,
+					"reconciled": 0,
+					"reference_type": next[0].reference_type,
+					"reference_name": next[0].reference_name,
+				},
+				fields=["*"],
+				order_by="idx",
+			)
+
+		return allocations
+	return []
+
+
+def fetch_and_allocate(doc: str) -> None:
+	"""
+	Fetch Invoices and Payments based on filters applied. FIFO ordering is used for allocation.
+	"""
+
+	if doc:
+		log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
+		if log:
+			if not frappe.db.get_value("Process Payment Reconciliation Log", log, "allocated"):
+				reconcile_log = frappe.get_doc("Process Payment Reconciliation Log", log)
+
+				pr = get_pr_instance(doc)
+				pr.get_unreconciled_entries()
+
+				if len(pr.invoices) > 0 and len(pr.payments) > 0:
+					invoices = [x.as_dict() for x in pr.invoices]
+					payments = [x.as_dict() for x in pr.payments]
+					pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+					for x in pr.get("allocation"):
+						reconcile_log.append(
+							"allocations",
+							x.as_dict().update(
+								{
+									"parenttype": "Process Payment Reconciliation Log",
+									"parent": reconcile_log.name,
+									"name": None,
+									"reconciled": False,
+								}
+							),
+						)
+				reconcile_log.allocated = True
+				reconcile_log.total_allocations = len(reconcile_log.get("allocations"))
+				reconcile_log.reconciled_entries = 0
+				reconcile_log.save()
+
+				# generate reconcile job name
+				allocation = get_next_allocation(log)
+				if allocation:
+					reconcile_job_name = (
+						f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
+					)
+				else:
+					reconcile_job_name = f"process_{doc}_reconcile"
+
+				if not is_job_running(reconcile_job_name):
+					job = frappe.enqueue(
+						method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
+						queue="long",
+						timeout="3600",
+						is_async=True,
+						job_name=reconcile_job_name,
+						enqueue_after_commit=True,
+						doc=doc,
+					)
+
+
+def reconcile(doc: None | str = None) -> None:
+	if doc:
+		log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
+		if log:
+			res = frappe.get_all(
+				"Process Payment Reconciliation Log",
+				filters={"name": log},
+				fields=["reconciled_entries", "total_allocations"],
+				as_list=1,
+				limit=1,
+			)
+
+			reconciled_entries, total_allocations = res[0]
+			if reconciled_entries != total_allocations:
+				try:
+					# Fetch next allocation
+					allocations = get_next_allocation(log)
+
+					pr = get_pr_instance(doc)
+
+					# pass allocation to PR instance
+					for x in allocations:
+						pr.append("allocation", x)
+
+					# reconcile
+					pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
+
+					# If Payment Entry, update details only for newly linked references
+					# This is for performance
+					if allocations[0].reference_type == "Payment Entry":
+
+						references = [(x.invoice_type, x.invoice_number) for x in allocations]
+						pe = frappe.get_doc(allocations[0].reference_type, allocations[0].reference_name)
+						pe.flags.ignore_validate_update_after_submit = True
+						pe.set_missing_ref_details(update_ref_details_only_for=references)
+						pe.save()
+
+					# Update reconciled flag
+					allocation_names = [x.name for x in allocations]
+					ppa = qb.DocType("Process Payment Reconciliation Log Allocations")
+					qb.update(ppa).set(ppa.reconciled, True).where(ppa.name.isin(allocation_names)).run()
+
+					# Update reconciled count
+					reconciled_count = frappe.db.count(
+						"Process Payment Reconciliation Log Allocations", filters={"parent": log, "reconciled": True}
+					)
+					frappe.db.set_value(
+						"Process Payment Reconciliation Log", log, "reconciled_entries", reconciled_count
+					)
+
+				except Exception as err:
+					# Update the parent doc about the exception
+					frappe.db.rollback()
+
+					traceback = frappe.get_traceback()
+					if traceback:
+						message = "Traceback: <br>" + traceback
+						frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)
+						frappe.db.set_value(
+							"Process Payment Reconciliation",
+							doc,
+							"error_log",
+							message,
+						)
+					if reconciled_entries and total_allocations and reconciled_entries < total_allocations:
+						frappe.db.set_value(
+							"Process Payment Reconciliation Log", log, "status", "Partially Reconciled"
+						)
+						frappe.db.set_value(
+							"Process Payment Reconciliation",
+							doc,
+							"status",
+							"Partially Reconciled",
+						)
+					else:
+						frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Failed")
+						frappe.db.set_value(
+							"Process Payment Reconciliation",
+							doc,
+							"status",
+							"Failed",
+						)
+				finally:
+					if reconciled_entries == total_allocations:
+						frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
+						frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
+						frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
+					else:
+
+						if not (frappe.db.get_value("Process Payment Reconciliation", doc, "status") == "Paused"):
+							# trigger next batch in job
+							# generate reconcile job name
+							allocation = get_next_allocation(log)
+							if allocation:
+								reconcile_job_name = (
+									f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
+								)
+							else:
+								reconcile_job_name = f"process_{doc}_reconcile"
+
+							if not is_job_running(reconcile_job_name):
+								job = frappe.enqueue(
+									method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
+									queue="long",
+									timeout="3600",
+									is_async=True,
+									job_name=reconcile_job_name,
+									enqueue_after_commit=True,
+									doc=doc,
+								)
+			else:
+				frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
+				frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
+				frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
+
+
+@frappe.whitelist()
+def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
+	running_doc = None
+	if for_filter:
+		if type(for_filter) == str:
+			for_filter = frappe.json.loads(for_filter)
+
+		running_doc = frappe.db.get_value(
+			"Process Payment Reconciliation",
+			filters={
+				"docstatus": 1,
+				"status": ["in", ["Running", "Paused"]],
+				"company": for_filter.get("company"),
+				"party_type": for_filter.get("party_type"),
+				"party": for_filter.get("party"),
+				"receivable_payable_account": for_filter.get("receivable_payable_account"),
+			},
+			fieldname="name",
+		)
+	else:
+		running_doc = frappe.db.get_value(
+			"Process Payment Reconciliation", filters={"docstatus": 1, "status": "Running"}
+		)
+	return running_doc
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_dashboard.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_dashboard.py
new file mode 100644
index 0000000..784f454
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_dashboard.py
@@ -0,0 +1,15 @@
+from frappe import _
+
+
+def get_data():
+	return {
+		"fieldname": "process_pr",
+		"transactions": [
+			{
+				"label": _("Reconciliation Logs"),
+				"items": [
+					"Process Payment Reconciliation Log",
+				],
+			},
+		],
+	}
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js
new file mode 100644
index 0000000..8012d6e
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js
@@ -0,0 +1,15 @@
+frappe.listview_settings['Process Payment Reconciliation'] = {
+	add_fields: ["status"],
+	get_indicator: function(doc) {
+		let colors = {
+			'Queued': 'orange',
+			'Paused': 'orange',
+			'Completed': 'green',
+			'Partially Reconciled': 'orange',
+			'Running': 'blue',
+			'Failed': 'red',
+		};
+		let status = doc.status;
+		return [__(status), colors[status], 'status,=,'+status];
+	},
+};
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/test_process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/test_process_payment_reconciliation.py
new file mode 100644
index 0000000..ad1e952
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/test_process_payment_reconciliation.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestProcessPaymentReconciliation(FrappeTestCase):
+	pass
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/__init__.py b/erpnext/accounts/doctype/process_payment_reconciliation_log/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/__init__.py
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js
new file mode 100644
index 0000000..2468f10
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Process Payment Reconciliation Log", {
+	refresh(frm) {
+		if (['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
+			let progress = 0;
+			if (frm.doc.reconciled_entries != 0) {
+				progress = frm.doc.reconciled_entries / frm.doc.total_allocations * 100;
+			} else if(frm.doc.total_allocations == 0 && frm.doc.status == "Completed"){
+				progress = 100;
+			}
+			frm.dashboard.add_progress(__('Reconciliation Progress'), progress);
+		}
+
+	},
+});
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json
new file mode 100644
index 0000000..1131a0f
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json
@@ -0,0 +1,137 @@
+{
+ "actions": [],
+ "autoname": "format:PPR-LOG-{##}",
+ "beta": 1,
+ "creation": "2023-03-13 15:00:09.149681",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "process_pr",
+  "section_break_fvdw",
+  "status",
+  "tasks_section",
+  "allocated",
+  "reconciled",
+  "column_break_yhin",
+  "total_allocations",
+  "reconciled_entries",
+  "section_break_4ywv",
+  "error_log",
+  "allocations_section",
+  "allocations"
+ ],
+ "fields": [
+  {
+   "fieldname": "allocations",
+   "fieldtype": "Table",
+   "label": "Allocations",
+   "options": "Process Payment Reconciliation Log Allocations",
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "description": "All allocations have been successfully reconciled",
+   "fieldname": "reconciled",
+   "fieldtype": "Check",
+   "label": "Reconciled",
+   "read_only": 1
+  },
+  {
+   "fieldname": "total_allocations",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Total Allocations",
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "description": "Invoices and Payments have been Fetched and Allocated",
+   "fieldname": "allocated",
+   "fieldtype": "Check",
+   "label": "Allocated",
+   "read_only": 1
+  },
+  {
+   "fieldname": "reconciled_entries",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Reconciled Entries",
+   "read_only": 1
+  },
+  {
+   "fieldname": "tasks_section",
+   "fieldtype": "Section Break",
+   "label": "Tasks"
+  },
+  {
+   "fieldname": "allocations_section",
+   "fieldtype": "Section Break",
+   "label": "Allocations"
+  },
+  {
+   "fieldname": "column_break_yhin",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_4ywv",
+   "fieldtype": "Section Break"
+  },
+  {
+   "depends_on": "eval:doc.error_log",
+   "fieldname": "error_log",
+   "fieldtype": "Long Text",
+   "label": "Reconciliation Error Log",
+   "read_only": 1
+  },
+  {
+   "fieldname": "process_pr",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Parent Document",
+   "options": "Process Payment Reconciliation",
+   "read_only": 1,
+   "reqd": 1
+  },
+  {
+   "fieldname": "section_break_fvdw",
+   "fieldtype": "Section Break",
+   "label": "Status"
+  },
+  {
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "label": "Status",
+   "options": "Running\nPaused\nReconciled\nPartially Reconciled\nFailed\nCancelled",
+   "read_only": 1
+  }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2023-04-21 17:36:26.642617",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Process Payment Reconciliation Log",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "search_fields": "allocated, reconciled, total_allocations, reconciled_entries",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.py b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.py
new file mode 100644
index 0000000..85d70a4
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class ProcessPaymentReconciliationLog(Document):
+	pass
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js
new file mode 100644
index 0000000..5a65204
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js
@@ -0,0 +1,15 @@
+frappe.listview_settings['Process Payment Reconciliation Log'] = {
+	add_fields: ["status"],
+	get_indicator: function(doc) {
+		var colors = {
+			'Partially Reconciled': 'orange',
+			'Paused': 'orange',
+			'Reconciled': 'green',
+			'Failed': 'red',
+			'Cancelled': 'red',
+			'Running': 'blue',
+		};
+		let status = doc.status;
+		return [__(status), colors[status], "status,=,"+status];
+	},
+};
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/test_process_payment_reconciliation_log.py b/erpnext/accounts/doctype/process_payment_reconciliation_log/test_process_payment_reconciliation_log.py
new file mode 100644
index 0000000..c2da62e
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/test_process_payment_reconciliation_log.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestProcessPaymentReconciliationLog(FrappeTestCase):
+	pass
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/__init__.py b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/__init__.py
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json
new file mode 100644
index 0000000..b97d738
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json
@@ -0,0 +1,170 @@
+{
+ "actions": [],
+ "creation": "2023-03-13 13:51:27.351463",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "reference_type",
+  "reference_name",
+  "reference_row",
+  "column_break_3",
+  "invoice_type",
+  "invoice_number",
+  "section_break_6",
+  "allocated_amount",
+  "unreconciled_amount",
+  "column_break_8",
+  "amount",
+  "is_advance",
+  "section_break_5",
+  "difference_amount",
+  "column_break_7",
+  "difference_account",
+  "exchange_rate",
+  "currency",
+  "reconciled"
+ ],
+ "fields": [
+  {
+   "fieldname": "reference_type",
+   "fieldtype": "Link",
+   "label": "Reference Type",
+   "options": "DocType",
+   "read_only": 1,
+   "reqd": 1
+  },
+  {
+   "fieldname": "reference_name",
+   "fieldtype": "Dynamic Link",
+   "in_list_view": 1,
+   "label": "Reference Name",
+   "options": "reference_type",
+   "read_only": 1,
+   "reqd": 1
+  },
+  {
+   "fieldname": "reference_row",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Reference Row",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_3",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "invoice_type",
+   "fieldtype": "Link",
+   "label": "Invoice Type",
+   "options": "DocType",
+   "read_only": 1,
+   "reqd": 1
+  },
+  {
+   "fieldname": "invoice_number",
+   "fieldtype": "Dynamic Link",
+   "in_list_view": 1,
+   "label": "Invoice Number",
+   "options": "invoice_type",
+   "read_only": 1,
+   "reqd": 1
+  },
+  {
+   "fieldname": "section_break_6",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "allocated_amount",
+   "fieldtype": "Currency",
+   "in_list_view": 1,
+   "label": "Allocated Amount",
+   "options": "currency",
+   "reqd": 1
+  },
+  {
+   "fieldname": "unreconciled_amount",
+   "fieldtype": "Currency",
+   "hidden": 1,
+   "label": "Unreconciled Amount",
+   "options": "currency",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_8",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "amount",
+   "fieldtype": "Currency",
+   "hidden": 1,
+   "label": "Amount",
+   "options": "currency",
+   "read_only": 1
+  },
+  {
+   "fieldname": "is_advance",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Is Advance",
+   "read_only": 1
+  },
+  {
+   "fieldname": "section_break_5",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "difference_amount",
+   "fieldtype": "Currency",
+   "in_list_view": 1,
+   "label": "Difference Amount",
+   "options": "Currency",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_7",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "difference_account",
+   "fieldtype": "Link",
+   "label": "Difference Account",
+   "options": "Account",
+   "read_only": 1
+  },
+  {
+   "fieldname": "exchange_rate",
+   "fieldtype": "Float",
+   "label": "Exchange Rate",
+   "read_only": 1
+  },
+  {
+   "fieldname": "currency",
+   "fieldtype": "Link",
+   "hidden": 1,
+   "label": "Currency",
+   "options": "Currency"
+  },
+  {
+   "default": "0",
+   "fieldname": "reconciled",
+   "fieldtype": "Check",
+   "in_list_view": 1,
+   "label": "Reconciled"
+  }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2023-03-20 21:05:43.121945",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Process Payment Reconciliation Log Allocations",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py
new file mode 100644
index 0000000..c3e4329
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class ProcessPaymentReconciliationLogAllocations(Document):
+	pass
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 56e412b..8cb2950 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -334,6 +334,7 @@
 	}
 
 	make_inter_company_invoice() {
+		let me = this;
 		frappe.model.open_mapped_doc({
 			method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice",
 			frm: me.frm
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index f10cff0..890872f 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -436,7 +436,7 @@
 	return cc.name
 
 
-def reconcile_against_document(args):  # nosemgrep
+def reconcile_against_document(args, skip_ref_details_update_for_pe=False):  # nosemgrep
 	"""
 	Cancel PE or JV, Update against document, split if required and resubmit
 	"""
@@ -465,7 +465,9 @@
 			if voucher_type == "Journal Entry":
 				update_reference_in_journal_entry(entry, doc, do_not_save=True)
 			else:
-				update_reference_in_payment_entry(entry, doc, do_not_save=True)
+				update_reference_in_payment_entry(
+					entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
+				)
 
 		doc.save(ignore_permissions=True)
 		# re-submit advance entry
@@ -602,7 +604,9 @@
 		journal_entry.save(ignore_permissions=True)
 
 
-def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
+def update_reference_in_payment_entry(
+	d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False
+):
 	reference_details = {
 		"reference_doctype": d.against_voucher_type,
 		"reference_name": d.against_voucher,
@@ -646,7 +650,8 @@
 	payment_entry.flags.ignore_validate_update_after_submit = True
 	payment_entry.setup_party_account_field()
 	payment_entry.set_missing_values()
-	payment_entry.set_missing_ref_details()
+	if not skip_ref_details_update_for_pe:
+		payment_entry.set_missing_ref_details()
 	payment_entry.set_amounts()
 
 	if not do_not_save:
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index c741622..642d51c 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -5,7 +5,7 @@
 import json
 
 import frappe
-from frappe import _, throw
+from frappe import _, bold, throw
 from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
 from frappe.query_builder.functions import Abs, Sum
 from frappe.utils import (
@@ -405,6 +405,15 @@
 				msg += _("Please create purchase from internal sale or delivery document itself")
 				frappe.throw(msg, title=_("Internal Sales Reference Missing"))
 
+			label = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item"
+
+			field = frappe.scrub(label)
+
+			for row in self.get("items"):
+				if not row.get(field):
+					msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
+					frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
+
 	def disable_pricing_rule_on_internal_transfer(self):
 		if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
 			self.ignore_pricing_rule = 1
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 862a546..02b301e 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -362,6 +362,7 @@
 	"cron": {
 		"0/15 * * * *": [
 			"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
+			"erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs",
 		],
 		"0/30 * * * *": [
 			"erpnext.utilities.doctype.video.video.update_youtube_data",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index a085af8..b53149a 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1317,7 +1317,7 @@
 		if not field in searchfields
 	]
 
-	query_filters = {"disabled": 0, "end_of_life": (">", today())}
+	query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())}
 
 	or_cond_filters = {}
 	if txt:
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index e82f379..f899516 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -74,6 +74,37 @@
 		self.update_sub_operation_status()
 		self.validate_work_order()
 
+	def on_update(self):
+		self.validate_job_card_qty()
+
+	def validate_job_card_qty(self):
+		if not (self.operation_id and self.work_order):
+			return
+
+		wo_qty = flt(frappe.get_cached_value("Work Order", self.work_order, "qty"))
+
+		completed_qty = flt(
+			frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty")
+		)
+
+		job_card_qty = frappe.get_all(
+			"Job Card",
+			fields=["sum(for_quantity)"],
+			filters={
+				"work_order": self.work_order,
+				"operation_id": self.operation_id,
+				"docstatus": ["!=", 2],
+			},
+			as_list=1,
+		)
+
+		job_card_qty = flt(job_card_qty[0][0]) if job_card_qty else 0
+
+		if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
+			msg = f"""Job Card quantity cannot be greater than
+				Work Order quantity for the operation {self.operation}"""
+			frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
+
 	def set_sub_operations(self):
 		if not self.sub_operations and self.operation:
 			self.sub_operations = []
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 729ed42..540b7dc 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -1598,6 +1598,57 @@
 			self.assertEqual(row.to_time, add_to_date(planned_start_date, minutes=30))
 			self.assertEqual(row.workstation, workstations_to_check[index])
 
+	def test_job_card_extra_qty(self):
+		items = [
+			"Test FG Item for Scrap Item Test 1",
+			"Test RM Item 1 for Scrap Item Test 1",
+			"Test RM Item 2 for Scrap Item Test 1",
+		]
+
+		company = "_Test Company with perpetual inventory"
+		for item_code in items:
+			create_item(
+				item_code=item_code,
+				is_stock_item=1,
+				is_purchase_item=1,
+				opening_stock=100,
+				valuation_rate=10,
+				company=company,
+				warehouse="Stores - TCP1",
+			)
+
+		item = "Test FG Item for Scrap Item Test 1"
+		raw_materials = ["Test RM Item 1 for Scrap Item Test 1", "Test RM Item 2 for Scrap Item Test 1"]
+		if not frappe.db.get_value("BOM", {"item": item}):
+			bom = make_bom(
+				item=item, source_warehouse="Stores - TCP1", raw_materials=raw_materials, do_not_save=True
+			)
+			bom.with_operations = 1
+			bom.append(
+				"operations",
+				{
+					"operation": "_Test Operation 1",
+					"workstation": "_Test Workstation 1",
+					"hour_rate": 20,
+					"time_in_mins": 60,
+				},
+			)
+
+			bom.submit()
+
+		wo_order = make_wo_order_test_record(
+			item=item,
+			company=company,
+			planned_start_date=now(),
+			qty=20,
+		)
+		job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name")
+		job_card_doc = frappe.get_doc("Job Card", job_card)
+
+		# Make another Job Card for the same Work Order
+		job_card2 = frappe.copy_doc(job_card_doc)
+		self.assertRaises(frappe.ValidationError, job_card2.save)
+
 
 def prepare_data_for_workstation_type_check():
 	from erpnext.manufacturing.doctype.operation.test_operation import make_operation
diff --git "a/erpnext/regional/report/fichier_des_ecritures_comptables_\133fec\135/fichier_des_ecritures_comptables_\133fec\135.py" "b/erpnext/regional/report/fichier_des_ecritures_comptables_\133fec\135/fichier_des_ecritures_comptables_\133fec\135.py"
index c75179e..6717989 100644
--- "a/erpnext/regional/report/fichier_des_ecritures_comptables_\133fec\135/fichier_des_ecritures_comptables_\133fec\135.py"
+++ "b/erpnext/regional/report/fichier_des_ecritures_comptables_\133fec\135/fichier_des_ecritures_comptables_\133fec\135.py"
@@ -1,31 +1,135 @@
 # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 
-
 import re
 
 import frappe
 from frappe import _
 from frappe.utils import format_datetime
 
+COLUMNS = [
+	{
+		"label": "JournalCode",
+		"fieldname": "JournalCode",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "JournalLib",
+		"fieldname": "JournalLib",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "EcritureNum",
+		"fieldname": "EcritureNum",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "EcritureDate",
+		"fieldname": "EcritureDate",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "CompteNum",
+		"fieldname": "CompteNum",
+		"fieldtype": "Link",
+		"options": "Account",
+		"width": 100,
+	},
+	{
+		"label": "CompteLib",
+		"fieldname": "CompteLib",
+		"fieldtype": "Link",
+		"options": "Account",
+		"width": 200,
+	},
+	{
+		"label": "CompAuxNum",
+		"fieldname": "CompAuxNum",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "CompAuxLib",
+		"fieldname": "CompAuxLib",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "PieceRef",
+		"fieldname": "PieceRef",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "PieceDate",
+		"fieldname": "PieceDate",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "EcritureLib",
+		"fieldname": "EcritureLib",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "Debit",
+		"fieldname": "Debit",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "Credit",
+		"fieldname": "Credit",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "EcritureLet",
+		"fieldname": "EcritureLet",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "DateLet",
+		"fieldname": "DateLet",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "ValidDate",
+		"fieldname": "ValidDate",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "Montantdevise",
+		"fieldname": "Montantdevise",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+	{
+		"label": "Idevise",
+		"fieldname": "Idevise",
+		"fieldtype": "Data",
+		"width": 90,
+	},
+]
+
 
 def execute(filters=None):
-	account_details = {}
-	for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1):
-		account_details.setdefault(acc.name, acc)
-
-	validate_filters(filters, account_details)
-
-	filters = set_account_currency(filters)
-
-	columns = get_columns(filters)
-
-	res = get_result(filters)
-
-	return columns, res
+	validate_filters(filters)
+	return COLUMNS, get_result(
+		company=filters["company"],
+		fiscal_year=filters["fiscal_year"],
+	)
 
 
-def validate_filters(filters, account_details):
+def validate_filters(filters):
 	if not filters.get("company"):
 		frappe.throw(_("{0} is mandatory").format(_("Company")))
 
@@ -33,107 +137,96 @@
 		frappe.throw(_("{0} is mandatory").format(_("Fiscal Year")))
 
 
-def set_account_currency(filters):
+def get_gl_entries(company, fiscal_year):
+	gle = frappe.qb.DocType("GL Entry")
+	sales_invoice = frappe.qb.DocType("Sales Invoice")
+	purchase_invoice = frappe.qb.DocType("Purchase Invoice")
+	journal_entry = frappe.qb.DocType("Journal Entry")
+	payment_entry = frappe.qb.DocType("Payment Entry")
+	customer = frappe.qb.DocType("Customer")
+	supplier = frappe.qb.DocType("Supplier")
+	employee = frappe.qb.DocType("Employee")
 
-	filters["company_currency"] = frappe.get_cached_value(
-		"Company", filters.company, "default_currency"
+	debit = frappe.query_builder.functions.Sum(gle.debit).as_("debit")
+	credit = frappe.query_builder.functions.Sum(gle.credit).as_("credit")
+	debit_currency = frappe.query_builder.functions.Sum(gle.debit_in_account_currency).as_(
+		"debitCurr"
+	)
+	credit_currency = frappe.query_builder.functions.Sum(gle.credit_in_account_currency).as_(
+		"creditCurr"
 	)
 
-	return filters
-
-
-def get_columns(filters):
-	columns = [
-		"JournalCode" + "::90",
-		"JournalLib" + "::90",
-		"EcritureNum" + ":Dynamic Link:90",
-		"EcritureDate" + "::90",
-		"CompteNum" + ":Link/Account:100",
-		"CompteLib" + ":Link/Account:200",
-		"CompAuxNum" + "::90",
-		"CompAuxLib" + "::90",
-		"PieceRef" + "::90",
-		"PieceDate" + "::90",
-		"EcritureLib" + "::90",
-		"Debit" + "::90",
-		"Credit" + "::90",
-		"EcritureLet" + "::90",
-		"DateLet" + "::90",
-		"ValidDate" + "::90",
-		"Montantdevise" + "::90",
-		"Idevise" + "::90",
-	]
-
-	return columns
-
-
-def get_result(filters):
-	gl_entries = get_gl_entries(filters)
-
-	result = get_result_as_list(gl_entries, filters)
-
-	return result
-
-
-def get_gl_entries(filters):
-
-	group_by_condition = (
-		"group by voucher_type, voucher_no, account"
-		if filters.get("group_by_voucher")
-		else "group by gl.name"
+	query = (
+		frappe.qb.from_(gle)
+		.left_join(sales_invoice)
+		.on(gle.voucher_no == sales_invoice.name)
+		.left_join(purchase_invoice)
+		.on(gle.voucher_no == purchase_invoice.name)
+		.left_join(journal_entry)
+		.on(gle.voucher_no == journal_entry.name)
+		.left_join(payment_entry)
+		.on(gle.voucher_no == payment_entry.name)
+		.left_join(customer)
+		.on(gle.party == customer.name)
+		.left_join(supplier)
+		.on(gle.party == supplier.name)
+		.left_join(employee)
+		.on(gle.party == employee.name)
+		.select(
+			gle.posting_date.as_("GlPostDate"),
+			gle.name.as_("GlName"),
+			gle.account,
+			gle.transaction_date,
+			debit,
+			credit,
+			debit_currency,
+			credit_currency,
+			gle.voucher_type,
+			gle.voucher_no,
+			gle.against_voucher_type,
+			gle.against_voucher,
+			gle.account_currency,
+			gle.against,
+			gle.party_type,
+			gle.party,
+			sales_invoice.name.as_("InvName"),
+			sales_invoice.title.as_("InvTitle"),
+			sales_invoice.posting_date.as_("InvPostDate"),
+			purchase_invoice.name.as_("PurName"),
+			purchase_invoice.title.as_("PurTitle"),
+			purchase_invoice.posting_date.as_("PurPostDate"),
+			journal_entry.cheque_no.as_("JnlRef"),
+			journal_entry.posting_date.as_("JnlPostDate"),
+			journal_entry.title.as_("JnlTitle"),
+			payment_entry.name.as_("PayName"),
+			payment_entry.posting_date.as_("PayPostDate"),
+			payment_entry.title.as_("PayTitle"),
+			customer.customer_name,
+			customer.name.as_("cusName"),
+			supplier.supplier_name,
+			supplier.name.as_("supName"),
+			employee.employee_name,
+			employee.name.as_("empName"),
+		)
+		.where((gle.company == company) & (gle.fiscal_year == fiscal_year))
+		.groupby(gle.voucher_type, gle.voucher_no, gle.account)
+		.orderby(gle.posting_date, gle.voucher_no)
 	)
 
-	gl_entries = frappe.db.sql(
-		"""
-		select
-			gl.posting_date as GlPostDate, gl.name as GlName, gl.account, gl.transaction_date,
-			sum(gl.debit) as debit, sum(gl.credit) as credit,
-			sum(gl.debit_in_account_currency) as debitCurr, sum(gl.credit_in_account_currency) as creditCurr,
-			gl.voucher_type, gl.voucher_no, gl.against_voucher_type,
-			gl.against_voucher, gl.account_currency, gl.against,
-			gl.party_type, gl.party,
-			inv.name as InvName, inv.title as InvTitle, inv.posting_date as InvPostDate,
-			pur.name as PurName, pur.title as PurTitle, pur.posting_date as PurPostDate,
-			jnl.cheque_no as JnlRef, jnl.posting_date as JnlPostDate, jnl.title as JnlTitle,
-			pay.name as PayName, pay.posting_date as PayPostDate, pay.title as PayTitle,
-			cus.customer_name, cus.name as cusName,
-			sup.supplier_name, sup.name as supName,
-			emp.employee_name, emp.name as empName,
-			stu.title as student_name, stu.name as stuName,
-			member_name, mem.name as memName
-
-		from `tabGL Entry` gl
-			left join `tabSales Invoice` inv on gl.voucher_no = inv.name
-			left join `tabPurchase Invoice` pur on gl.voucher_no = pur.name
-			left join `tabJournal Entry` jnl on gl.voucher_no = jnl.name
-			left join `tabPayment Entry` pay on gl.voucher_no = pay.name
-			left join `tabCustomer` cus on gl.party = cus.name
-			left join `tabSupplier` sup on gl.party = sup.name
-			left join `tabEmployee` emp on gl.party = emp.name
-			left join `tabStudent` stu on gl.party = stu.name
-			left join `tabMember` mem on gl.party = mem.name
-		where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s
-		{group_by_condition}
-		order by GlPostDate, voucher_no""".format(
-			group_by_condition=group_by_condition
-		),
-		filters,
-		as_dict=1,
-	)
-
-	return gl_entries
+	return query.run(as_dict=True)
 
 
-def get_result_as_list(data, filters):
+def get_result(company, fiscal_year):
+	data = get_gl_entries(company, fiscal_year)
+
 	result = []
 
-	company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
+	company_currency = frappe.get_cached_value("Company", company, "default_currency")
 	accounts = frappe.get_all(
-		"Account", filters={"Company": filters.company}, fields=["name", "account_number"]
+		"Account", filters={"Company": company}, fields=["name", "account_number"]
 	)
 
 	for d in data:
-
 		JournalCode = re.split("-|/|[0-9]", d.get("voucher_no"))[0]
 
 		if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith(
@@ -141,9 +234,7 @@
 		):
 			EcritureNum = re.split("-|/", d.get("voucher_no"))[1]
 		else:
-			EcritureNum = re.search(
-				r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE
-			).group(1)
+			EcritureNum = re.search(r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE)[1]
 
 		EcritureDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd")
 
@@ -185,7 +276,7 @@
 
 		ValidDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd")
 
-		PieceRef = d.get("voucher_no") if d.get("voucher_no") else "Sans Reference"
+		PieceRef = d.get("voucher_no") or "Sans Reference"
 
 		# EcritureLib is the reference title unless it is an opening entry
 		if d.get("is_opening") == "Yes":
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index ccea840..4f498fb 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -30,10 +30,6 @@
   "cost_center",
   "dimension_col_break",
   "project",
-  "column_break_77",
-  "source",
-  "campaign",
-  "custom_dimensions_section",
   "currency_and_price_list",
   "currency",
   "conversion_rate",
@@ -162,7 +158,9 @@
   "is_internal_customer",
   "represents_company",
   "column_break_152",
+  "source",
   "inter_company_order_reference",
+  "campaign",
   "party_account_currency",
   "connections_tab"
  ],
@@ -1165,12 +1163,6 @@
    "read_only": 1
   },
   {
-   "fieldname": "column_break_77",
-   "fieldtype": "Column Break",
-   "hide_days": 1,
-   "hide_seconds": 1
-  },
-  {
    "fieldname": "source",
    "fieldtype": "Link",
    "hide_days": 1,
@@ -1613,10 +1605,6 @@
    "fieldtype": "Column Break"
   },
   {
-   "fieldname": "custom_dimensions_section",
-   "fieldtype": "Section Break"
-  },
-  {
    "collapsible": 1,
    "fieldname": "additional_info_section",
    "fieldtype": "Section Break",
@@ -1643,7 +1631,7 @@
  "idx": 105,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-12-12 18:34:00.681780",
+ "modified": "2023-04-20 11:14:01.036202",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Sales Order",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 0c1f820..2adf9c3 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -28,8 +28,6 @@
   "column_break_18",
   "project",
   "dimension_col_break",
-  "campaign",
-  "source",
   "custom_dimensions_section",
   "currency_and_price_list",
   "currency",
@@ -161,11 +159,12 @@
   "inter_company_reference",
   "customer_group",
   "territory",
+  "source",
+  "campaign",
   "column_break5",
   "excise_page",
   "instructions",
-  "connections_tab",
-  "column_break_25"
+  "connections_tab"
  ],
  "fields": [
   {
@@ -1340,10 +1339,6 @@
    "fieldtype": "Column Break"
   },
   {
-   "fieldname": "column_break_25",
-   "fieldtype": "Column Break"
-  },
-  {
    "fieldname": "section_break_30",
    "fieldtype": "Section Break",
    "hide_border": 1
@@ -1403,7 +1398,7 @@
  "idx": 146,
  "is_submittable": 1,
  "links": [],
- "modified": "2023-02-14 04:45:44.179670",
+ "modified": "2023-04-21 11:15:23.931084",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Delivery Note",
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 8af279a..c34f9da 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1693,6 +1693,64 @@
 		# Value of Stock Account should be equal to the sum of Stock Value Difference
 		self.assertEqual(stock_account_value, stock_diff)
 
+	def test_internal_pr_reference(self):
+		item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100})
+		customer = "_Test Internal Customer 2"
+		company = "_Test Company with perpetual inventory"
+		from_warehouse = create_warehouse("_Test Internal From Warehouse New 1", company=company)
+		target_warehouse = create_warehouse("_Test Internal GIT Warehouse New 1", company=company)
+		to_warehouse = create_warehouse("_Test Internal To Warehouse New 1", company=company)
+
+		# Step 2: Create Stock Entry (Material Receipt)
+		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+		make_stock_entry(
+			purpose="Material Receipt",
+			item_code=item.name,
+			qty=15,
+			company=company,
+			to_warehouse=from_warehouse,
+		)
+
+		# Step 3: Create Delivery Note with Internal Customer
+		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+		dn = create_delivery_note(
+			item_code=item.name,
+			company=company,
+			customer=customer,
+			cost_center="Main - TCP1",
+			expense_account="Cost of Goods Sold - TCP1",
+			qty=10,
+			rate=100,
+			warehouse=from_warehouse,
+			target_warehouse=target_warehouse,
+		)
+
+		# Step 4: Create Internal Purchase Receipt
+		from erpnext.controllers.status_updater import OverAllowanceError
+		from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+
+		pr = make_inter_company_purchase_receipt(dn.name)
+		pr.inter_company_reference = ""
+		self.assertRaises(frappe.ValidationError, pr.save)
+
+		pr.inter_company_reference = dn.name
+		pr.items[0].qty = 10
+		pr.items[0].from_warehouse = target_warehouse
+		pr.items[0].warehouse = to_warehouse
+		pr.items[0].rejected_warehouse = from_warehouse
+		pr.save()
+
+		delivery_note_item = pr.items[0].delivery_note_item
+		pr.items[0].delivery_note_item = ""
+
+		self.assertRaises(frappe.ValidationError, pr.save)
+
+		pr.load_from_db()
+		pr.items[0].delivery_note_item = delivery_note_item
+		pr.save()
+
 
 def prepare_data_for_internal_transfer():
 	from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 0f12987..82fc0df 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -1388,7 +1388,11 @@
 def regenerate_sle_for_batch_stock_reco(detail):
 	doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
 	doc.recalculate_current_qty(detail.item_code, detail.batch_no)
-	doc.repost_future_sle_and_gle()
+
+	if not frappe.db.exists(
+		"Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
+	):
+		doc.repost_future_sle_and_gle()
 
 
 def get_stock_reco_qty_shift(args):
@@ -1441,13 +1445,13 @@
 				(
 					CombineDatetime(sle.posting_date, sle.posting_time)
 					> CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
-					| (
-						(
-							CombineDatetime(sle.posting_date, sle.posting_time)
-							== CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
-						)
-						& (sle.creation > kwargs.get("creation"))
+				)
+				| (
+					(
+						CombineDatetime(sle.posting_date, sle.posting_time)
+						== CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
 					)
+					& (sle.creation > kwargs.get("creation"))
 				)
 			)
 		)
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
index 1698e23..1c6f24b 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
@@ -192,7 +192,7 @@
   }
  ],
  "links": [],
- "modified": "2021-11-26 15:45:33.289911",
+ "modified": "2023-04-21 17:16:56.192560",
  "modified_by": "Administrator",
  "module": "Support",
  "name": "Service Level Agreement",
@@ -212,19 +212,12 @@
    "write": 1
   },
   {
-   "create": 1,
-   "delete": 1,
-   "email": 1,
-   "export": 1,
-   "print": 1,
    "read": 1,
-   "report": 1,
-   "role": "All",
-   "share": 1,
-   "write": 1
+   "role": "All"
   }
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file