Merge branch 'develop' into bug_invoice_creation_throws_typeerror
diff --git a/cypress/integration/test_bulk_transaction_processing.js b/cypress/integration/test_bulk_transaction_processing.js
new file mode 100644
index 0000000..428ec51
--- /dev/null
+++ b/cypress/integration/test_bulk_transaction_processing.js
@@ -0,0 +1,44 @@
+describe("Bulk Transaction Processing", () => {
+	before(() => {
+		cy.login();
+		cy.visit("/app/website");
+	});
+
+	it("Creates To Sales Order", () => {
+		cy.visit("/app/sales-order");
+		cy.url().should("include", "/sales-order");
+		cy.window()
+			.its("frappe.csrf_token")
+			.then((csrf_token) => {
+				return cy
+					.request({
+						url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records",
+						method: "POST",
+						headers: {
+							Accept: "application/json",
+							"Content-Type": "application/json",
+							"X-Frappe-CSRF-Token": csrf_token,
+						},
+						timeout: 60000,
+					})
+					.then((res) => {
+						expect(res.status).eq(200);
+					});
+			});
+		cy.wait(5000);
+		cy.get(
+			".list-row-head > .list-header-subject > .list-row-col > .list-check-all"
+		).check({ force: true });
+		cy.wait(3000);
+		cy.get(".actions-btn-group > .btn-primary").click({ force: true });
+		cy.wait(3000);
+		cy.get(".dropdown-menu-right > .user-action > .dropdown-item")
+			.contains("Sales Invoice")
+			.click({ force: true });
+		cy.wait(3000);
+		cy.get(".modal-content > .modal-footer > .standard-actions")
+			.contains("Yes")
+			.click({ force: true });
+		cy.contains("Creation of Sales Invoice successful");
+	});
+});
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 76d9cc7..2c31561 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -178,8 +178,8 @@
 
 		if self.supplier and account.account_type != "Payable":
 			frappe.throw(
-				_("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.")
-				.format(frappe.bold("Credit To")), title=_("Invalid Account")
+				_("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.")
+				.format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account")
 			)
 
 		self.party_account_currency = account.account_currency
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
index f6ff83a..82d0030 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
@@ -56,4 +56,14 @@
 			];
 		}
 	},
+
+	onload: function(listview) {
+		listview.page.add_action_item(__("Purchase Receipt"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
+		});
+
+		listview.page.add_action_item(__("Payment"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment");
+		});
+	}
 };
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index bc44358..754ca81 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -572,7 +572,10 @@
 			frappe.throw(msg, title=_("Invalid Account"))
 
 		if self.customer and account.account_type != "Receivable":
-			msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " "
+			msg = _("Please ensure {} account {} is a Receivable account.").format(
+				frappe.bold("Debit To"),
+				frappe.bold(self.debit_to)
+			) + " "
 			msg += _("Change the account type to Receivable or select a different account.")
 			frappe.throw(msg, title=_("Invalid Account"))
 
@@ -1249,14 +1252,14 @@
 	def update_billing_status_in_dn(self, update_modified=True):
 		updated_delivery_notes = []
 		for d in self.get("items"):
-			if d.dn_detail:
+			if d.so_detail:
+				updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
+			elif d.dn_detail:
 				billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
 					where dn_detail=%s and docstatus=1""", d.dn_detail)
 				billed_amt = billed_amt and billed_amt[0][0] or 0
 				frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified)
 				updated_delivery_notes.append(d.delivery_note)
-			elif d.so_detail:
-				updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
 
 		for dn in set(updated_delivery_notes):
 			frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js
index 06e6f51..1130284 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js
@@ -21,5 +21,15 @@
 		};
 		return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
 	},
-	right_column: "grand_total"
+	right_column: "grand_total",
+
+	onload: function(listview) {
+		listview.page.add_action_item(__("Delivery Note"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
+		});
+
+		listview.page.add_action_item(__("Payment"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment");
+		});
+	}
 };
diff --git a/erpnext/bulk_transaction/__init__.py b/erpnext/bulk_transaction/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/bulk_transaction/__init__.py
diff --git a/erpnext/bulk_transaction/doctype/__init__.py b/erpnext/bulk_transaction/doctype/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/__init__.py
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/__init__.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/__init__.py
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js
new file mode 100644
index 0000000..a739cc3
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js
@@ -0,0 +1,34 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Bulk Transaction Log', {
+
+	before_load: function(frm) {
+		query(frm);
+	},
+
+	refresh: function(frm) {
+		frm.disable_save();
+		frm.add_custom_button(__('Retry Failed Transactions'), ()=>{
+			frappe.confirm(__("Retry Failing Transactions ?"), ()=>{
+				query(frm);
+			}
+			);
+		});
+	}
+});
+
+function query(frm) {
+	frappe.call({
+		method: "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
+		args: {
+			log_date: frm.doc.log_date
+		}
+	}).then((r) => {
+		if (r.message) {
+			frm.remove_custom_button("Retry Failed Transactions");
+		} else {
+			frappe.show_alert(__("Retrying Failed Transactions"), 5);
+		}
+	});
+}
\ No newline at end of file
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json
new file mode 100644
index 0000000..da42cf1
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json
@@ -0,0 +1,51 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-11-30 13:41:16.343827",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "log_date",
+  "logger_data"
+ ],
+ "fields": [
+  {
+   "fieldname": "log_date",
+   "fieldtype": "Date",
+   "label": "Log Date",
+   "read_only": 1
+  },
+  {
+   "fieldname": "logger_data",
+   "fieldtype": "Table",
+   "label": "Logger Data",
+   "options": "Bulk Transaction Log Detail"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2022-02-03 17:23:02.935325",
+ "modified_by": "Administrator",
+ "module": "Bulk Transaction",
+ "name": "Bulk Transaction Log",
+ "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": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py
new file mode 100644
index 0000000..de7cde5
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py
@@ -0,0 +1,66 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from datetime import date
+
+import frappe
+from frappe.model.document import Document
+
+from erpnext.utilities.bulk_transaction import task, update_logger
+
+
+class BulkTransactionLog(Document):
+	pass
+
+
+@frappe.whitelist()
+def retry_failing_transaction(log_date=None):
+	btp = frappe.qb.DocType("Bulk Transaction Log Detail")
+	data = (
+		frappe.qb.from_(btp)
+		.select(btp.transaction_name, btp.from_doctype, btp.to_doctype)
+		.distinct()
+		.where(btp.retried != 1)
+		.where(btp.transaction_status == "Failed")
+		.where(btp.date == log_date)
+	).run(as_dict=True)
+
+	if data:
+		if not log_date:
+			log_date = str(date.today())
+		if len(data) > 10:
+			frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date)
+		else:
+			job(data, log_date)
+	else:
+		return "No Failed Records"
+
+def job(data, log_date):
+	for d in data:
+		failed = []
+		try:
+			frappe.db.savepoint("before_creation_of_record")
+			task(d.transaction_name, d.from_doctype, d.to_doctype)
+		except Exception as e:
+			frappe.db.rollback(save_point="before_creation_of_record")
+			failed.append(e)
+			update_logger(
+				d.transaction_name,
+				e,
+				d.from_doctype,
+				d.to_doctype,
+				status="Failed",
+				log_date=log_date,
+				restarted=1
+			)
+
+		if not failed:
+			update_logger(
+				d.transaction_name,
+				None,
+				d.from_doctype,
+				d.to_doctype,
+				status="Success",
+				log_date=log_date,
+				restarted=1,
+			)
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py
new file mode 100644
index 0000000..a78e697
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py
@@ -0,0 +1,81 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import unittest
+from datetime import date
+
+import frappe
+
+from erpnext.utilities.bulk_transaction import transaction_processing
+
+
+class TestBulkTransactionLog(unittest.TestCase):
+
+	def setUp(self):
+		create_company()
+		create_customer()
+		create_item()
+
+	def test_for_single_record(self):
+		so_name = create_so()
+		transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
+		data = frappe.db.get_list("Sales Invoice", filters = {"posting_date": date.today(), "customer": "Bulk Customer"}, fields=["*"])
+		if not data:
+			self.fail("No Sales Invoice Created !")
+
+	def test_entry_in_log(self):
+		so_name = create_so()
+		transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
+		doc = frappe.get_doc("Bulk Transaction Log", str(date.today()))
+		for d in doc.get("logger_data"):
+			if d.transaction_name == so_name:
+				self.assertEqual(d.transaction_name, so_name)
+				self.assertEqual(d.transaction_status, "Success")
+				self.assertEqual(d.from_doctype, "Sales Order")
+				self.assertEqual(d.to_doctype, "Sales Invoice")
+				self.assertEqual(d.retried, 0)
+
+
+
+def create_company():
+	if not frappe.db.exists('Company', '_Test Company'):
+		frappe.get_doc({
+			'doctype': 'Company',
+			'company_name': '_Test Company',
+			'country': 'India',
+			'default_currency': 'INR'
+		}).insert()
+
+def create_customer():
+	if not frappe.db.exists('Customer', 'Bulk Customer'):
+		frappe.get_doc({
+			'doctype': 'Customer',
+			'customer_name': 'Bulk Customer'
+		}).insert()
+
+def create_item():
+	if not frappe.db.exists("Item", "MK"):
+		frappe.get_doc({
+			"doctype": "Item",
+			"item_code": "MK",
+			"item_name": "Milk",
+			"description": "Milk",
+			"item_group": "Products"
+		}).insert()
+
+def create_so(intent=None):
+	so = frappe.new_doc("Sales Order")
+	so.customer = "Bulk Customer"
+	so.company = "_Test Company"
+	so.transaction_date = date.today()
+
+	so.set_warehouse = "Finished Goods - _TC"
+	so.append("items", {
+		"item_code": "MK",
+		"delivery_date": date.today(),
+		"qty": 10,
+		"rate": 80,
+	})
+	so.insert()
+	so.submit()
+	return so.name
\ No newline at end of file
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/__init__.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/__init__.py
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json
new file mode 100644
index 0000000..8262caa
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json
@@ -0,0 +1,86 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-11-30 13:38:30.926047",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "transaction_name",
+  "date",
+  "time",
+  "transaction_status",
+  "error_description",
+  "from_doctype",
+  "to_doctype",
+  "retried"
+ ],
+ "fields": [
+  {
+   "fieldname": "transaction_name",
+   "fieldtype": "Dynamic Link",
+   "in_list_view": 1,
+   "label": "Name",
+   "options": "from_doctype"
+  },
+  {
+   "fieldname": "transaction_status",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Status",
+   "read_only": 1
+  },
+  {
+   "fieldname": "error_description",
+   "fieldtype": "Long Text",
+   "label": "Error Description",
+   "read_only": 1
+  },
+  {
+   "fieldname": "from_doctype",
+   "fieldtype": "Link",
+   "label": "From Doctype",
+   "options": "DocType",
+   "read_only": 1
+  },
+  {
+   "fieldname": "to_doctype",
+   "fieldtype": "Link",
+   "label": "To Doctype",
+   "options": "DocType",
+   "read_only": 1
+  },
+  {
+   "fieldname": "date",
+   "fieldtype": "Date",
+   "in_list_view": 1,
+   "label": "Date ",
+   "read_only": 1
+  },
+  {
+   "fieldname": "time",
+   "fieldtype": "Time",
+   "label": "Time",
+   "read_only": 1
+  },
+  {
+   "fieldname": "retried",
+   "fieldtype": "Int",
+   "label": "Retried",
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-02-03 19:57:31.650359",
+ "modified_by": "Administrator",
+ "module": "Bulk Transaction",
+ "name": "Bulk Transaction Log Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.py
new file mode 100644
index 0000000..67795b9
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BulkTransactionLogDetail(Document):
+	pass
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_list.js b/erpnext/buying/doctype/purchase_order/purchase_order_list.js
index 8413eb6..d7907e4 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order_list.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order_list.js
@@ -29,8 +29,22 @@
 			listview.call_for_selected_items(method, { "status": "Closed" });
 		});
 
-		listview.page.add_menu_item(__("Re-open"), function () {
+		listview.page.add_menu_item(__("Reopen"), function () {
 			listview.call_for_selected_items(method, { "status": "Submitted" });
 		});
+
+
+		listview.page.add_action_item(__("Purchase Invoice"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice");
+		});
+
+		listview.page.add_action_item(__("Purchase Receipt"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt");
+		});
+
+		listview.page.add_action_item(__("Advance Payment"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment");
+		});
+
 	}
 };
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
index d65ab94..171de78 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
@@ -143,6 +143,26 @@
 	return doclist
 
 @frappe.whitelist()
+def make_purchase_invoice(source_name, target_doc=None):
+	doc = get_mapped_doc("Supplier Quotation", source_name, {
+		"Supplier Quotation": {
+			"doctype": "Purchase Invoice",
+			"validation": {
+				"docstatus": ["=", 1],
+			}
+		},
+		"Supplier Quotation Item": {
+			"doctype": "Purchase Invoice Item"
+		},
+		"Purchase Taxes and Charges": {
+			"doctype": "Purchase Taxes and Charges"
+		}
+	}, target_doc)
+
+	return doc
+
+
+@frappe.whitelist()
 def make_quotation(source_name, target_doc=None):
 	doclist = get_mapped_doc("Supplier Quotation", source_name, {
 		"Supplier Quotation": {
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js
index 5ab6c98..73685ca 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js
@@ -8,5 +8,15 @@
 		} else if(doc.status==="Expired") {
 			return [__("Expired"), "gray", "status,=,Expired"];
 		}
+	},
+
+	onload: function(listview) {
+		listview.page.add_action_item(__("Purchase Order"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order");
+		});
+
+		listview.page.add_action_item(__("Purchase Invoice"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice");
+		});
 	}
 };
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 8d17683..c8e5edd 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -3,6 +3,7 @@
 
 import json
 from collections import defaultdict
+from typing import List, Tuple
 
 import frappe
 from frappe import _
@@ -181,33 +182,28 @@
 
 			return details
 
-	def get_items_and_warehouses(self):
-		items, warehouses = [], []
+	def get_items_and_warehouses(self) -> Tuple[List[str], List[str]]:
+		"""Get list of items and warehouses affected by a transaction"""
 
-		if hasattr(self, "items"):
-			item_doclist = self.get("items")
-		elif self.doctype == "Stock Reconciliation":
-			item_doclist = []
-			data = json.loads(self.reconciliation_json)
-			for row in data[data.index(self.head_row)+1:]:
-				d = frappe._dict(zip(["item_code", "warehouse", "qty", "valuation_rate"], row))
-				item_doclist.append(d)
+		if not (hasattr(self, "items") or hasattr(self, "packed_items")):
+			return [], []
 
-		if item_doclist:
-			for d in item_doclist:
-				if d.item_code and d.item_code not in items:
-					items.append(d.item_code)
+		item_rows = (self.get("items") or []) + (self.get("packed_items") or [])
 
-				if d.get("warehouse") and d.warehouse not in warehouses:
-					warehouses.append(d.warehouse)
+		items = {d.item_code for d in item_rows if d.item_code}
 
-				if self.doctype == "Stock Entry":
-					if d.get("s_warehouse") and d.s_warehouse not in warehouses:
-						warehouses.append(d.s_warehouse)
-					if d.get("t_warehouse") and d.t_warehouse not in warehouses:
-						warehouses.append(d.t_warehouse)
+		warehouses = set()
+		for d in item_rows:
+			if d.get("warehouse"):
+				warehouses.add(d.warehouse)
 
-		return items, warehouses
+			if self.doctype == "Stock Entry":
+				if d.get("s_warehouse"):
+					warehouses.add(d.s_warehouse)
+				if d.get("t_warehouse"):
+					warehouses.add(d.t_warehouse)
+
+		return list(items), list(warehouses)
 
 	def get_stock_ledger_details(self):
 		stock_ledger = {}
@@ -219,7 +215,7 @@
 			from
 				`tabStock Ledger Entry`
 			where
-				voucher_type=%s and voucher_no=%s
+				voucher_type=%s and voucher_no=%s and is_cancelled = 0
 		""", (self.doctype, self.name), as_dict=True)
 
 		for sle in stock_ledger_entries:
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 0e29038..d99f23e 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -341,7 +341,8 @@
 		"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts"
 	],
 	"hourly_long": [
-		"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
+		"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
+		"erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction"
 	],
 	"daily": [
 		"erpnext.stock.reorder_item.reorder_item",
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index c5705c1..8c79ee5 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -21,4 +21,5 @@
 Loan Management
 Payroll
 Telephony
+Bulk Transaction
 E-commerce
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 569910d..91a752c 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -39,7 +39,8 @@
 		"public/js/utils/dimension_tree_filter.js",
 		"public/js/telephony.js",
 		"public/js/templates/call_link.html",
-		"public/js/templates/node_card.html"
+		"public/js/templates/node_card.html",
+		"public/js/bulk_transaction_processing.js"
 	],
 	"js/item-dashboard.min.js": [
 		"stock/dashboard/item_dashboard.html",
diff --git a/erpnext/public/js/bulk_transaction_processing.js b/erpnext/public/js/bulk_transaction_processing.js
new file mode 100644
index 0000000..101f50c
--- /dev/null
+++ b/erpnext/public/js/bulk_transaction_processing.js
@@ -0,0 +1,30 @@
+frappe.provide("erpnext.bulk_transaction_processing");
+
+$.extend(erpnext.bulk_transaction_processing, {
+	create: function(listview, from_doctype, to_doctype) {
+		let checked_items = listview.get_checked_items();
+		const doc_name = [];
+		checked_items.forEach((Item)=> {
+			if (Item.docstatus == 0) {
+				doc_name.push(Item.name);
+			}
+		});
+
+		let count_of_rows = checked_items.length;
+		frappe.confirm(__("Create {0} {1} ?", [count_of_rows, to_doctype]), ()=>{
+			if (doc_name.length == 0) {
+				frappe.call({
+					method: "erpnext.utilities.bulk_transaction.transaction_processing",
+					args: {data: checked_items, from_doctype: from_doctype, to_doctype: to_doctype}
+				}).then(()=> {
+
+				});
+				if (count_of_rows > 10) {
+					frappe.show_alert("Starting a background job to create {0} {1}", [count_of_rows, to_doctype]);
+				}
+			} else {
+				frappe.msgprint(__("Selected document must be in submitted state"));
+			}
+		});
+	}
+});
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 3791741..ab3e802 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -2288,7 +2288,8 @@
 				() => this.frm.doc.ignore_pricing_rule=1,
 				() => me.ignore_pricing_rule(),
 				() => this.frm.doc.ignore_pricing_rule=0,
-				() => me.apply_pricing_rule()
+				() => me.apply_pricing_rule(),
+				() => this.frm.save()
 			]);
 		} else {
 			frappe.run_serially([
diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js
index 5259bdc..b3a68b3 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -22,5 +22,6 @@
 import "./utils/dimension_tree_filter";
 import "./telephony";
 import "./templates/call_link.html";
+import "./bulk_transaction_processing";
 
 // import { sum } from 'frappe/public/utils/util.js'
diff --git a/erpnext/selling/doctype/quotation/quotation_list.js b/erpnext/selling/doctype/quotation/quotation_list.js
index b631685..4c8f9c4 100644
--- a/erpnext/selling/doctype/quotation/quotation_list.js
+++ b/erpnext/selling/doctype/quotation/quotation_list.js
@@ -12,6 +12,14 @@
 				};
 			};
 		}
+
+		listview.page.add_action_item(__("Sales Order"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order");
+		});
+
+		listview.page.add_action_item(__("Sales Invoice"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice");
+		});
 	},
 
 	get_indicator: function(doc) {
diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js
index 26d96d5..4691190 100644
--- a/erpnext/selling/doctype/sales_order/sales_order_list.js
+++ b/erpnext/selling/doctype/sales_order/sales_order_list.js
@@ -16,7 +16,7 @@
 				return [__("Overdue"), "red",
 					"per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"];
 			} else if (flt(doc.grand_total) === 0) {
-				// not delivered (zero-amount order)
+				// not delivered (zeroount order)
 				return [__("To Deliver"), "orange",
 					"per_delivered,<,100|grand_total,=,0|status,!=,Closed"];
 			} else if (flt(doc.per_billed, 6) < 100) {
@@ -48,5 +48,17 @@
 			listview.call_for_selected_items(method, {"status": "Submitted"});
 		});
 
+		listview.page.add_action_item(__("Sales Invoice"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice");
+		});
+
+		listview.page.add_action_item(__("Delivery Note"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note");
+		});
+
+		listview.page.add_action_item(__("Advance Payment"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Advance Payment");
+		});
+
 	}
 };
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index d1e2244..2a4d639 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -339,17 +339,35 @@
 			frappe.throw(_("Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again"))
 
 def update_billed_amount_based_on_so(so_detail, update_modified=True):
+	from frappe.query_builder.functions import Sum
+
 	# Billed against Sales Order directly
-	billed_against_so = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
-		where so_detail=%s and (dn_detail is null or dn_detail = '') and docstatus=1""", so_detail)
+	si = frappe.qb.DocType("Sales Invoice").as_("si")
+	si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item")
+	sum_amount = Sum(si_item.amount).as_("amount")
+
+	billed_against_so = frappe.qb.from_(si).from_(si_item).select(sum_amount).where(
+		(si_item.parent == si.name) &
+		(si_item.so_detail == so_detail) &
+		((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) &
+		(si_item.docstatus == 1) &
+		(si.update_stock == 0)
+	).run()
 	billed_against_so = billed_against_so and billed_against_so[0][0] or 0
 
 	# Get all Delivery Note Item rows against the Sales Order Item row
-	dn_details = frappe.db.sql("""select dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent
-		from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
-		where dn.name=dn_item.parent and dn_item.so_detail=%s
-			and dn.docstatus=1 and dn.is_return = 0
-		order by dn.posting_date asc, dn.posting_time asc, dn.name asc""", so_detail, as_dict=1)
+
+	dn = frappe.qb.DocType("Delivery Note").as_("dn")
+	dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item")
+
+	dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty).where(
+		(dn.name == dn_item.parent) &
+		(dn_item.so_detail == so_detail) &
+		(dn.docstatus == 1) &
+		(dn.is_return == 0)
+	).orderby(
+		dn.posting_date, dn.posting_time, dn.name
+	).run(as_dict=True)
 
 	updated_dn = []
 	for dnd in dn_details:
@@ -367,7 +385,11 @@
 
 		# Distribute billed amount directly against SO between DNs based on FIFO
 		if billed_against_so and billed_amt_agianst_dn < dnd.amount:
-			pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn
+			if dnd.returned_qty:
+				pending_to_bill = flt(dnd.amount) * (dnd.stock_qty - dnd.returned_qty) / dnd.stock_qty
+			else:
+				pending_to_bill = flt(dnd.amount)
+			pending_to_bill -= billed_amt_agianst_dn
 			if pending_to_bill <= billed_against_so:
 				billed_amt_agianst_dn += pending_to_bill
 				billed_against_so -= pending_to_bill
@@ -586,7 +608,18 @@
 			"validation": {
 				"docstatus": ["=", 0]
 			}
+		},
+
+		"Delivery Note Item": {
+			"doctype": "Packing Slip Item",
+			"field_map": {
+				"item_code": "item_code",
+				"item_name": "item_name",
+				"description": "description",
+				"qty": "qty",
+			}
 		}
+
 	}, target_doc)
 
 	return doclist
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
index 0402898..9e6f3bc 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
@@ -14,7 +14,7 @@
 			return [__("Completed"), "green", "per_billed,=,100"];
 		}
 	},
-	onload: function (doclist) {
+	onload: function (listview) {
 		const action = () => {
 			const selected_docs = doclist.get_checked_items();
 			const docnames = doclist.get_checked_items(true);
@@ -54,6 +54,16 @@
 			};
 		};
 
-		doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false);
+		// doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false);
+
+		listview.page.add_action_item(__('Create Delivery Trip'), action);
+
+		listview.page.add_action_item(__("Sales Invoice"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Sales Invoice");
+		});
+
+		listview.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Packing Slip");
+		});
 	}
 };
diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py
index 5cbaa1e..2521ac9 100644
--- a/erpnext/stock/doctype/packed_item/test_packed_item.py
+++ b/erpnext/stock/doctype/packed_item/test_packed_item.py
@@ -1,10 +1,14 @@
 # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
+from frappe.utils import add_to_date, nowdate
+
 from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
 from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
 from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
 from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
 from erpnext.tests.utils import ERPNextTestCase, change_settings
 
 
@@ -12,31 +16,30 @@
 	"Test impact on Packed Items table in various scenarios."
 	@classmethod
 	def setUpClass(cls) -> None:
-		make_item("_Test Product Bundle X", {"is_stock_item": 0})
-		make_item("_Test Bundle Item 1", {"is_stock_item": 1})
-		make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+		super().setUpClass()
+		cls.bundle = "_Test Product Bundle X"
+		cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"]
+		make_item(cls.bundle, {"is_stock_item": 0})
+		for item in cls.bundle_items:
+			make_item(item, {"is_stock_item": 1})
+
 		make_item("_Test Normal Stock Item", {"is_stock_item": 1})
 
-		make_product_bundle(
-			"_Test Product Bundle X",
-			["_Test Bundle Item 1", "_Test Bundle Item 2"],
-			qty=2
-		)
+		make_product_bundle(cls.bundle, cls.bundle_items, qty=2)
 
 	def test_adding_bundle_item(self):
 		"Test impact on packed items if bundle item row is added."
-		so = make_sales_order(item_code = "_Test Product Bundle X", qty=1,
+		so = make_sales_order(item_code = self.bundle, qty=1,
 			do_not_submit=True)
 
 		self.assertEqual(so.items[0].qty, 1)
 		self.assertEqual(len(so.packed_items), 2)
-		self.assertEqual(so.packed_items[0].item_code, "_Test Bundle Item 1")
+		self.assertEqual(so.packed_items[0].item_code, self.bundle_items[0])
 		self.assertEqual(so.packed_items[0].qty, 2)
 
 	def test_updating_bundle_item(self):
 		"Test impact on packed items if bundle item row is updated."
-		so = make_sales_order(item_code = "_Test Product Bundle X", qty=1,
-			do_not_submit=True)
+		so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True)
 
 		so.items[0].qty = 2 # change qty
 		so.save()
@@ -55,7 +58,7 @@
 		so_items = []
 		for qty in [2, 4, 6, 8]:
 			so_items.append({
-				"item_code": "_Test Product Bundle X",
+				"item_code": self.bundle,
 				"qty": qty,
 				"rate": 400,
 				"warehouse": "_Test Warehouse - _TC"
@@ -66,7 +69,7 @@
 
 		# check alternate rows for qty
 		self.assertEqual(len(so.packed_items), 8)
-		self.assertEqual(so.packed_items[1].item_code, "_Test Bundle Item 2")
+		self.assertEqual(so.packed_items[1].item_code, self.bundle_items[1])
 		self.assertEqual(so.packed_items[1].qty, 4)
 		self.assertEqual(so.packed_items[3].qty, 8)
 		self.assertEqual(so.packed_items[5].qty, 12)
@@ -94,8 +97,7 @@
 	@change_settings("Selling Settings", {"editable_bundle_item_rates": 1})
 	def test_bundle_item_cumulative_price(self):
 		"Test if Bundle Item rate is cumulative from packed items."
-		so = make_sales_order(item_code = "_Test Product Bundle X", qty=2,
-			do_not_submit=True)
+		so = make_sales_order(item_code=self.bundle, qty=2, do_not_submit=True)
 
 		so.packed_items[0].rate = 150
 		so.packed_items[1].rate = 200
@@ -109,7 +111,7 @@
 		so_items = []
 		for qty in [2, 4]:
 			so_items.append({
-				"item_code": "_Test Product Bundle X",
+				"item_code": self.bundle,
 				"qty": qty,
 				"rate": 400,
 				"warehouse": "_Test Warehouse - _TC"
@@ -124,4 +126,33 @@
 
 		self.assertEqual(len(dn.packed_items), 4)
 		self.assertEqual(dn.packed_items[2].qty, 6)
-		self.assertEqual(dn.packed_items[3].qty, 6)
\ No newline at end of file
+		self.assertEqual(dn.packed_items[3].qty, 6)
+
+	def test_reposting_packed_items(self):
+		warehouse = "Stores - TCP1"
+		company = "_Test Company with perpetual inventory"
+
+		today = nowdate()
+		yesterday = add_to_date(today, days=-1, as_string=True)
+
+		for item in self.bundle_items:
+			make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date=today)
+
+		so = make_sales_order(item_code = self.bundle, qty=1, company=company, warehouse=warehouse)
+
+		dn = make_delivery_note(so.name)
+		dn.save()
+		dn.submit()
+
+		gles = get_gl_entries(dn.doctype, dn.name)
+		credit_before_repost = sum(gle.credit for gle in gles)
+
+		# backdated stock entry
+		for item in self.bundle_items:
+			make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday)
+
+		# assert correct reposting
+		gles = get_gl_entries(dn.doctype, dn.name)
+		credit_after_reposting = sum(gle.credit for gle in gles)
+		self.assertNotEqual(credit_before_repost, credit_after_reposting)
+		self.assertAlmostEqual(credit_after_reposting, 2 * credit_before_repost)
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index ffdf8c4..33e40c8 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -288,9 +288,6 @@
 						{"voucher_type": "Purchase Receipt", "voucher_no": self.name,
 						"voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference")
 
-					if not stock_value_diff:
-						continue
-
 					warehouse_account_name = warehouse_account[d.warehouse]["account"]
 					warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"]
 					supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js
index 77711de..4029f0c 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js
@@ -13,5 +13,13 @@
 		} else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) {
 			return [__("Completed"), "green", "per_billed,=,100"];
 		}
+	},
+
+	onload: function(listview) {
+
+		listview.page.add_action_item(__("Purchase Invoice"), ()=>{
+			erpnext.bulk_transaction_processing.create(listview, "Purchase Receipt", "Purchase Invoice");
+		});
 	}
+
 };
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index b87d920..5ab7929 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -4,6 +4,7 @@
 
 import json
 import unittest
+from collections import defaultdict
 
 import frappe
 from frappe.utils import add_days, cint, cstr, flt, today
@@ -16,7 +17,7 @@
 from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
 from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
 from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
-from erpnext.tests.utils import ERPNextTestCase
+from erpnext.tests.utils import ERPNextTestCase, change_settings
 
 
 class TestPurchaseReceipt(ERPNextTestCase):
@@ -1387,6 +1388,36 @@
 
 		automatically_fetch_payment_terms(enable=0)
 
+	@change_settings("Stock Settings", {"allow_negative_stock": 1})
+	def test_neg_to_positive(self):
+		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+
+		item_code = "_TestNegToPosItem"
+		warehouse = "Stores - TCP1"
+		company = "_Test Company with perpetual inventory"
+		account = "Stock Received But Not Billed - TCP1"
+
+		make_item(item_code)
+		se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0)
+		se.items[0].allow_zero_valuation_rate = 1
+		se.save()
+		se.submit()
+
+		pr = make_purchase_receipt(
+			qty=50,
+			rate=1,
+			item_code=item_code,
+			warehouse=warehouse,
+			get_taxes_and_charges=True,
+			company=company,
+		)
+		gles = get_gl_entries(pr.doctype, pr.name)
+
+		for gle in gles:
+			if gle.account == account:
+				self.assertEqual(gle.credit, 50)
+
+
 def get_sl_entries(voucher_type, voucher_no):
 	return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
 		from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 01c5e3e..977d470 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -13,7 +13,7 @@
 	check_if_stock_and_account_balance_synced,
 	update_gl_entries_after,
 )
-from erpnext.stock.stock_ledger import repost_future_sle
+from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle
 
 
 class RepostItemValuation(Document):
@@ -138,13 +138,20 @@
 
 	if doc.based_on == 'Transaction':
 		ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no)
-		items, warehouses = ref_doc.get_items_and_warehouses()
+		doc_items, doc_warehouses = ref_doc.get_items_and_warehouses()
+
+		sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no)
+		sle_items = [sle.item_code for sle in sles]
+		sle_warehouse = [sle.warehouse for sle in sles]
+
+		items = list(set(doc_items).union(set(sle_items)))
+		warehouses = list(set(doc_warehouses).union(set(sle_warehouse)))
 	else:
 		items = [doc.item_code]
 		warehouses = [doc.warehouse]
 
 	update_gl_entries_after(doc.posting_date, doc.posting_time,
-		warehouses, items, company=doc.company)
+		for_warehouses=warehouses, for_items=items, company=doc.company)
 
 def notify_error_to_stock_managers(doc, traceback):
 	recipients = get_users_with_role("Stock Manager")
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
index 3402972..a882a61 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
@@ -18,7 +18,6 @@
   "items",
   "section_break_9",
   "expense_account",
-  "reconciliation_json",
   "column_break_13",
   "difference_amount",
   "amended_from",
@@ -112,15 +111,6 @@
    "options": "Cost Center"
   },
   {
-   "fieldname": "reconciliation_json",
-   "fieldtype": "Long Text",
-   "hidden": 1,
-   "label": "Reconciliation JSON",
-   "no_copy": 1,
-   "print_hide": 1,
-   "read_only": 1
-  },
-  {
    "fieldname": "column_break_13",
    "fieldtype": "Column Break"
   },
@@ -155,7 +145,7 @@
  "idx": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-11-30 01:33:51.437194",
+ "modified": "2022-02-06 14:28:19.043905",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Reconciliation",
@@ -178,5 +168,6 @@
  "search_fields": "posting_date",
  "show_name_in_global_search": 1,
  "sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 428370c..86af0a0 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -25,8 +25,8 @@
 class TestStockReconciliation(ERPNextTestCase):
 	@classmethod
 	def setUpClass(cls):
-		super().setUpClass()
 		create_batch_or_serial_no_items()
+		super().setUpClass()
 		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
 
 	def tearDown(self):
diff --git a/erpnext/tests/ui_test_bulk_transaction_processing.py b/erpnext/tests/ui_test_bulk_transaction_processing.py
new file mode 100644
index 0000000..d78689e
--- /dev/null
+++ b/erpnext/tests/ui_test_bulk_transaction_processing.py
@@ -0,0 +1,21 @@
+import frappe
+
+from erpnext.bulk_transaction.doctype.bulk_transaction_logger.test_bulk_transaction_logger import (
+	create_company,
+	create_customer,
+	create_item,
+	create_so,
+)
+
+
+@frappe.whitelist()
+def create_records():
+	create_company()
+	create_customer()
+	create_item()
+
+	gd = frappe.get_doc("Global Defaults")
+	gd.set("default_company", "Test Bulk")
+	gd.save()
+	frappe.clear_cache()
+	create_so()
\ No newline at end of file
diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py
new file mode 100644
index 0000000..64e2ff4
--- /dev/null
+++ b/erpnext/utilities/bulk_transaction.py
@@ -0,0 +1,201 @@
+import json
+from datetime import date, datetime
+
+import frappe
+from frappe import _
+
+
+@frappe.whitelist()
+def transaction_processing(data, from_doctype, to_doctype):
+	if isinstance(data, str):
+		deserialized_data = json.loads(data)
+
+	else:
+		deserialized_data = data
+
+	length_of_data = len(deserialized_data)
+
+	if length_of_data > 10:
+		frappe.msgprint(
+			_("Started a background job to create {1} {0}").format(to_doctype, length_of_data)
+		)
+		frappe.enqueue(
+			job,
+			deserialized_data=deserialized_data,
+			from_doctype=from_doctype,
+			to_doctype=to_doctype,
+		)
+	else:
+		job(deserialized_data, from_doctype, to_doctype)
+
+
+def job(deserialized_data, from_doctype, to_doctype):
+	failed_history = []
+	i = 0
+	for d in deserialized_data:
+		failed = []
+
+		try:
+			i += 1
+			doc_name = d.get("name")
+			frappe.db.savepoint("before_creation_state")
+			task(doc_name, from_doctype, to_doctype)
+
+		except Exception as e:
+			frappe.db.rollback(save_point="before_creation_state")
+			failed_history.append(e)
+			failed.append(e)
+			update_logger(doc_name, e, from_doctype, to_doctype, status="Failed", log_date=str(date.today()))
+		if not failed:
+			update_logger(doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today()))
+
+	show_job_status(failed_history, deserialized_data, to_doctype)
+
+
+def task(doc_name, from_doctype, to_doctype):
+	from erpnext.accounts.doctype.payment_entry import payment_entry
+	from erpnext.accounts.doctype.purchase_invoice import purchase_invoice
+	from erpnext.accounts.doctype.sales_invoice import sales_invoice
+	from erpnext.buying.doctype.purchase_order import purchase_order
+	from erpnext.buying.doctype.supplier_quotation import supplier_quotation
+	from erpnext.selling.doctype.quotation import quotation
+	from erpnext.selling.doctype.sales_order import sales_order
+	from erpnext.stock.doctype.delivery_note import delivery_note
+	from erpnext.stock.doctype.purchase_receipt import purchase_receipt
+
+	mapper = {
+		"Sales Order": {
+			"Sales Invoice": sales_order.make_sales_invoice,
+			"Delivery Note": sales_order.make_delivery_note,
+			"Advance Payment": payment_entry.get_payment_entry,
+		},
+		"Sales Invoice": {
+			"Delivery Note": sales_invoice.make_delivery_note,
+			"Payment": payment_entry.get_payment_entry,
+		},
+		"Delivery Note": {
+			"Sales Invoice": delivery_note.make_sales_invoice,
+			"Packing Slip": delivery_note.make_packing_slip,
+		},
+		"Quotation": {
+			"Sales Order": quotation.make_sales_order,
+			"Sales Invoice": quotation.make_sales_invoice,
+		},
+		"Supplier Quotation": {
+			"Purchase Order": supplier_quotation.make_purchase_order,
+			"Purchase Invoice": supplier_quotation.make_purchase_invoice,
+			"Advance Payment": payment_entry.get_payment_entry,
+		},
+		"Purchase Order": {
+			"Purchase Invoice": purchase_order.make_purchase_invoice,
+			"Purchase Receipt": purchase_order.make_purchase_receipt,
+		},
+		"Purhcase Invoice": {
+			"Purchase Receipt": purchase_invoice.make_purchase_receipt,
+			"Payment": payment_entry.get_payment_entry,
+		},
+		"Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice},
+	}
+	if to_doctype in ['Advance Payment', 'Payment']:
+		obj = mapper[from_doctype][to_doctype](from_doctype, doc_name)
+	else:
+		obj = mapper[from_doctype][to_doctype](doc_name)
+
+	obj.flags.ignore_validate = True
+	obj.insert(ignore_mandatory=True)
+
+
+def check_logger_doc_exists(log_date):
+	return frappe.db.exists("Bulk Transaction Log", log_date)
+
+
+def get_logger_doc(log_date):
+	return frappe.get_doc("Bulk Transaction Log", log_date)
+
+
+def create_logger_doc():
+	log_doc = frappe.new_doc("Bulk Transaction Log")
+	log_doc.set_new_name(set_name=str(date.today()))
+	log_doc.log_date = date.today()
+
+	return log_doc
+
+
+def append_data_to_logger(log_doc, doc_name, error, from_doctype, to_doctype, status, restarted):
+	row = log_doc.append("logger_data", {})
+	row.transaction_name = doc_name
+	row.date = date.today()
+	now = datetime.now()
+	row.time = now.strftime("%H:%M:%S")
+	row.transaction_status = status
+	row.error_description = str(error)
+	row.from_doctype = from_doctype
+	row.to_doctype = to_doctype
+	row.retried = restarted
+
+
+def update_logger(doc_name, e, from_doctype, to_doctype, status, log_date=None, restarted=0):
+	if not check_logger_doc_exists(log_date):
+		log_doc = create_logger_doc()
+		append_data_to_logger(log_doc, doc_name, e, from_doctype, to_doctype, status, restarted)
+		log_doc.insert()
+	else:
+		log_doc = get_logger_doc(log_date)
+		if record_exists(log_doc, doc_name, status):
+			append_data_to_logger(
+				log_doc, doc_name, e, from_doctype, to_doctype, status, restarted
+			)
+			log_doc.save()
+
+
+def show_job_status(failed_history, deserialized_data, to_doctype):
+	if not failed_history:
+		frappe.msgprint(
+			_("Creation of {0} successful").format(to_doctype),
+			title="Successful",
+			indicator="green",
+		)
+
+	if len(failed_history) != 0 and len(failed_history) < len(deserialized_data):
+		frappe.msgprint(
+			_("""Creation of {0} partially successful.
+				Check <b><a href="/app/bulk-transaction-log">Bulk Transaction Log</a></b>""").format(
+				to_doctype
+			),
+			title="Partially successful",
+			indicator="orange",
+		)
+
+	if len(failed_history) == len(deserialized_data):
+		frappe.msgprint(
+			_("""Creation of {0} failed.
+				Check <b><a href="/app/bulk-transaction-log">Bulk Transaction Log</a></b>""").format(
+				to_doctype
+			),
+			title="Failed",
+			indicator="red",
+		)
+
+
+def record_exists(log_doc, doc_name, status):
+
+	record = mark_retrired_transaction(log_doc, doc_name)
+
+	if record and status == "Failed":
+		return False
+	elif record and status == "Success":
+		return True
+	else:
+		return True
+
+
+def mark_retrired_transaction(log_doc, doc_name):
+	record = 0
+	for d in log_doc.get("logger_data"):
+		if d.transaction_name == doc_name and d.transaction_status == "Failed":
+			d.retried = 1
+			record = record + 1
+
+	log_doc.save()
+
+	return record
\ No newline at end of file