Merge branch 'develop' into remove-nonprofit
diff --git a/.github/stale.yml b/.github/stale.yml
index 8b7cb9b..1c2dcf3 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -30,6 +30,7 @@
   exemptLabels:
     - valid
     - to-validate
+    - QA
   markComment: >
     This issue has been automatically marked as inactive because it has not had
     recent activity and it wasn't validated by maintainer team. It will be
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/__init__.py b/erpnext/__init__.py
index a44c8fa..dcfad1f 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -2,8 +2,6 @@
 
 import frappe
 
-from erpnext.hooks import regional_overrides
-
 __version__ = '14.0.0-dev'
 
 def get_default_company(user=None):
@@ -121,12 +119,15 @@
 	@erpnext.allow_regional
 	def myfunction():
 	  pass'''
+
 	def caller(*args, **kwargs):
-		region = get_region()
-		fn_name = inspect.getmodule(fn).__name__ + '.' + fn.__name__
-		if region in regional_overrides and fn_name in regional_overrides[region]:
-			return frappe.get_attr(regional_overrides[region][fn_name])(*args, **kwargs)
-		else:
+		overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
+		function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"
+
+		if not overrides or function_path not in overrides:
 			return fn(*args, **kwargs)
 
+		# Priority given to last installed app
+		return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
+
 	return caller
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 55ea571..9a35a24 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -7,35 +7,30 @@
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
-  "accounts_transactions_settings_section",
-  "over_billing_allowance",
-  "role_allowed_to_over_bill",
-  "credit_controller",
-  "make_payment_via_journal_entry",
-  "column_break_11",
-  "check_supplier_invoice_uniqueness",
+  "invoice_and_billing_tab",
+  "enable_features_section",
   "unlink_payment_on_cancellation_of_invoice",
-  "automatically_fetch_payment_terms",
-  "delete_linked_ledger_entries",
-  "book_asset_depreciation_entry_automatically",
   "unlink_advance_payment_on_cancelation_of_order",
+  "column_break_13",
+  "delete_linked_ledger_entries",
+  "invoicing_features_section",
+  "check_supplier_invoice_uniqueness",
+  "automatically_fetch_payment_terms",
+  "column_break_17",
   "enable_common_party_accounting",
-  "post_change_gl_entries",
   "enable_discount_accounting",
-  "tax_settings_section",
-  "determine_address_tax_category_from",
-  "column_break_19",
-  "add_taxes_from_item_tax_template",
-  "period_closing_settings_section",
-  "acc_frozen_upto",
-  "frozen_accounts_modifier",
-  "column_break_4",
+  "report_setting_section",
+  "use_custom_cash_flow",
   "deferred_accounting_settings_section",
   "book_deferred_entries_based_on",
   "column_break_18",
   "automatically_process_deferred_accounting_entry",
   "book_deferred_entries_via_journal_entry",
   "submit_journal_entries",
+  "tax_settings_section",
+  "determine_address_tax_category_from",
+  "column_break_19",
+  "add_taxes_from_item_tax_template",
   "print_settings",
   "show_inclusive_tax_in_print",
   "column_break_12",
@@ -43,8 +38,25 @@
   "currency_exchange_section",
   "allow_stale",
   "stale_days",
-  "report_settings_sb",
-  "use_custom_cash_flow"
+  "invoicing_settings_tab",
+  "accounts_transactions_settings_section",
+  "over_billing_allowance",
+  "column_break_11",
+  "role_allowed_to_over_bill",
+  "credit_controller",
+  "make_payment_via_journal_entry",
+  "pos_tab",
+  "pos_setting_section",
+  "post_change_gl_entries",
+  "assets_tab",
+  "asset_settings_section",
+  "book_asset_depreciation_entry_automatically",
+  "closing_settings_tab",
+  "period_closing_settings_section",
+  "acc_frozen_upto",
+  "column_break_25",
+  "frozen_accounts_modifier",
+  "report_settings_sb"
  ],
  "fields": [
   {
@@ -71,10 +83,6 @@
    "options": "Billing Address\nShipping Address"
   },
   {
-   "fieldname": "column_break_4",
-   "fieldtype": "Column Break"
-  },
-  {
    "fieldname": "credit_controller",
    "fieldtype": "Link",
    "in_list_view": 1,
@@ -83,6 +91,7 @@
   },
   {
    "default": "0",
+   "description": "Enabling ensure each Sales Invoice has a unique value in Supplier Invoice No. field",
    "fieldname": "check_supplier_invoice_uniqueness",
    "fieldtype": "Check",
    "label": "Check Supplier Invoice Number Uniqueness"
@@ -168,7 +177,7 @@
    "description": "Only select this if you have set up the Cash Flow Mapper documents",
    "fieldname": "use_custom_cash_flow",
    "fieldtype": "Check",
-   "label": "Use Custom Cash Flow Format"
+   "label": "Enable Custom Cash Flow Format"
   },
   {
    "default": "0",
@@ -241,7 +250,7 @@
   {
    "fieldname": "accounts_transactions_settings_section",
    "fieldtype": "Section Break",
-   "label": "Transactions Settings"
+   "label": "Credit Limit Settings"
   },
   {
    "fieldname": "column_break_11",
@@ -272,9 +281,72 @@
   },
   {
    "default": "0",
+   "description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
    "fieldname": "enable_common_party_accounting",
    "fieldtype": "Check",
    "label": "Enable Common Party Accounting"
+  },
+  {
+   "fieldname": "enable_features_section",
+   "fieldtype": "Section Break",
+   "label": "Invoice Cancellation"
+  },
+  {
+   "fieldname": "column_break_13",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "column_break_25",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "asset_settings_section",
+   "fieldtype": "Section Break",
+   "label": "Asset Settings"
+  },
+  {
+   "fieldname": "invoicing_settings_tab",
+   "fieldtype": "Tab Break",
+   "label": "Credit Limits"
+  },
+  {
+   "fieldname": "assets_tab",
+   "fieldtype": "Tab Break",
+   "label": "Assets"
+  },
+  {
+   "fieldname": "closing_settings_tab",
+   "fieldtype": "Tab Break",
+   "label": "Accounts Closing"
+  },
+  {
+   "fieldname": "pos_setting_section",
+   "fieldtype": "Section Break",
+   "label": "POS Setting"
+  },
+  {
+   "fieldname": "invoice_and_billing_tab",
+   "fieldtype": "Tab Break",
+   "label": "Invoice and Billing"
+  },
+  {
+   "fieldname": "invoicing_features_section",
+   "fieldtype": "Section Break",
+   "label": "Invoicing Features"
+  },
+  {
+   "fieldname": "column_break_17",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "pos_tab",
+   "fieldtype": "Tab Break",
+   "label": "POS"
+  },
+  {
+   "fieldname": "report_setting_section",
+   "fieldtype": "Section Break",
+   "label": "Report Setting"
   }
  ],
  "icon": "icon-cog",
@@ -282,7 +354,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-10-11 17:42:36.427699",
+ "modified": "2022-02-04 12:32:36.805652",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Accounts Settings",
@@ -309,5 +381,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "ASC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
index 335f850..dbf3622 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -14,6 +14,10 @@
 		});
 	},
 
+	onload: function (frm) {
+		frm.trigger('bank_account');
+	},
+
 	refresh: function (frm) {
 		frappe.require("bank-reconciliation-tool.bundle.js", () =>
 			frm.trigger("make_reconciliation_tool")
@@ -51,7 +55,7 @@
 	bank_account: function (frm) {
 		frappe.db.get_value(
 			"Bank Account",
-			frm.bank_account,
+			frm.doc.bank_account,
 			"account",
 			(r) => {
 				frappe.db.get_value(
diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
index 77c9e95..b42d712 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
@@ -2,7 +2,7 @@
  "actions": [],
  "allow_import": 1,
  "allow_rename": 1,
- "creation": "2018-11-22 22:45:00.370913",
+ "creation": "2022-01-19 01:09:13.297137",
  "doctype": "DocType",
  "document_type": "Setup",
  "editable_grid": 1,
@@ -10,6 +10,9 @@
  "field_order": [
   "title",
   "company",
+  "column_break_3",
+  "disabled",
+  "section_break_5",
   "taxes"
  ],
  "fields": [
@@ -36,10 +39,24 @@
    "label": "Company",
    "options": "Company",
    "reqd": 1
+  },
+  {
+   "fieldname": "column_break_3",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "disabled",
+   "fieldtype": "Check",
+   "label": "Disabled"
+  },
+  {
+   "fieldname": "section_break_5",
+   "fieldtype": "Section Break"
   }
  ],
  "links": [],
- "modified": "2021-03-08 19:50:21.416513",
+ "modified": "2022-01-18 21:11:23.105589",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Item Tax Template",
@@ -82,6 +99,7 @@
  "show_name_in_global_search": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "title_field": "title",
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index 19d8d49..ade7f81 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -167,7 +167,8 @@
 			"is_pos": 0,
 			"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
 			"update_stock": 0,
-			"invoice_number": row.invoice_number
+			"invoice_number": row.invoice_number,
+			"disable_rounded_total": 1
 		})
 
 		accounting_dimension = get_accounting_dimensions()
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 97d34e0..5229d87 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -172,9 +172,10 @@
 			frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
 
 	def validate_stock_availablility(self):
+		from erpnext.stock.stock_ledger import is_negative_stock_allowed
+
 		if self.is_return or self.docstatus != 1:
 			return
-		allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
 		for d in self.get('items'):
 			is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
 			if is_service_item:
@@ -186,7 +187,7 @@
 			elif d.batch_no:
 				self.validate_pos_reserved_batch_qty(d)
 			else:
-				if allow_negative_stock:
+				if is_negative_stock_allowed(item_code=d.item_code):
 					return
 
 				available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index ba751c0..cf8affd 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -586,23 +586,29 @@
 		item_price.insert()
 		pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
 		pr.save()
-		pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
-		pos_inv.items[0].rate = 300
-		pos_inv.save()
-		self.assertEquals(pos_inv.items[0].discount_percentage, 10)
-		# rate shouldn't change
-		self.assertEquals(pos_inv.items[0].rate, 405)
 
-		pos_inv.ignore_pricing_rule = 1
-		pos_inv.items[0].rate = 300
-		pos_inv.save()
-		self.assertEquals(pos_inv.ignore_pricing_rule, 1)
-		# rate should change since pricing rules are ignored
-		self.assertEquals(pos_inv.items[0].rate, 300)
+		try:
+			pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
+			pos_inv.items[0].rate = 300
+			pos_inv.save()
+			self.assertEquals(pos_inv.items[0].discount_percentage, 10)
+			# rate shouldn't change
+			self.assertEquals(pos_inv.items[0].rate, 405)
 
-		item_price.delete()
-		pos_inv.delete()
-		pr.delete()
+			pos_inv.ignore_pricing_rule = 1
+			pos_inv.save()
+			self.assertEquals(pos_inv.ignore_pricing_rule, 1)
+			# rate should reset since pricing rules are ignored
+			self.assertEquals(pos_inv.items[0].rate, 450)
+
+			pos_inv.items[0].rate = 300
+			pos_inv.save()
+			self.assertEquals(pos_inv.items[0].rate, 300)
+
+		finally:
+			item_price.delete()
+			pos_inv.delete()
+			pr.delete()
 
 
 def create_pos_invoice(**args):
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 0720d9b..ddca68a 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -84,12 +84,20 @@
 		sales_invoice.set_posting_time = 1
 		sales_invoice.posting_date = getdate(self.posting_date)
 		sales_invoice.save()
+		self.write_off_fractional_amount(sales_invoice, data)
 		sales_invoice.submit()
 
 		self.consolidated_invoice = sales_invoice.name
 
 		return sales_invoice.name
 
+	def write_off_fractional_amount(self, invoice, data):
+		pos_invoice_grand_total = sum(d.grand_total for d in data)
+
+		if abs(pos_invoice_grand_total - invoice.grand_total) < 1:
+			invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total)
+			invoice.save()
+
 	def process_merging_into_credit_note(self, data):
 		credit_note = self.get_new_sales_invoice()
 		credit_note.is_return = 1
@@ -102,6 +110,7 @@
 		# TODO: return could be against multiple sales invoice which could also have been consolidated?
 		# credit_note.return_against = self.consolidated_invoice
 		credit_note.save()
+		self.write_off_fractional_amount(credit_note, data)
 		credit_note.submit()
 
 		self.consolidated_credit_note = credit_note.name
@@ -135,9 +144,15 @@
 						i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
 						found = True
 						i.qty = i.qty + item.qty
+						i.amount = i.amount + item.net_amount
+						i.net_amount = i.amount
+						i.base_amount = i.base_amount + item.base_net_amount
+						i.base_net_amount = i.base_amount
 
 				if not found:
 					item.rate = item.net_rate
+					item.amount = item.net_amount
+					item.base_amount = item.base_net_amount
 					item.price_list_rate = 0
 					si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
 					items.append(si_item)
@@ -169,6 +184,7 @@
 						found = True
 				if not found:
 					payments.append(payment)
+
 			rounding_adjustment += doc.rounding_adjustment
 			rounded_total += doc.rounded_total
 			base_rounding_adjustment += doc.base_rounding_adjustment
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
index 3555da8..5930aa0 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
@@ -12,6 +12,7 @@
 from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
 	consolidate_pos_invoices,
 )
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
 
 
 class TestPOSInvoiceMergeLog(unittest.TestCase):
@@ -150,3 +151,132 @@
 			frappe.set_user("Administrator")
 			frappe.db.sql("delete from `tabPOS Profile`")
 			frappe.db.sql("delete from `tabPOS Invoice`")
+
+
+	def test_consolidation_round_off_error_1(self):
+		'''
+		Test round off error in consolidated invoice creation if POS Invoice has inclusive tax
+		'''
+
+		frappe.db.sql("delete from `tabPOS Invoice`")
+
+		try:
+			make_stock_entry(
+				to_warehouse="_Test Warehouse - _TC",
+				item_code="_Test Item",
+				rate=8000,
+				qty=10,
+			)
+
+			init_user_and_profile()
+
+			inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
+			inv.append("taxes", {
+				"account_head": "_Test Account VAT - _TC",
+				"charge_type": "On Net Total",
+				"cost_center": "_Test Cost Center - _TC",
+				"description": "VAT",
+				"doctype": "Sales Taxes and Charges",
+				"rate": 7.5,
+				"included_in_print_rate": 1
+			})
+			inv.append('payments', {
+				'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
+			})
+			inv.insert()
+			inv.submit()
+
+			inv2 = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
+			inv2.append("taxes", {
+				"account_head": "_Test Account VAT - _TC",
+				"charge_type": "On Net Total",
+				"cost_center": "_Test Cost Center - _TC",
+				"description": "VAT",
+				"doctype": "Sales Taxes and Charges",
+				"rate": 7.5,
+				"included_in_print_rate": 1
+			})
+			inv2.append('payments', {
+				'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
+			})
+			inv2.insert()
+			inv2.submit()
+
+			consolidate_pos_invoices()
+
+			inv.load_from_db()
+			consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
+			self.assertEqual(consolidated_invoice.outstanding_amount, 0)
+			self.assertEqual(consolidated_invoice.status, 'Paid')
+
+		finally:
+			frappe.set_user("Administrator")
+			frappe.db.sql("delete from `tabPOS Profile`")
+			frappe.db.sql("delete from `tabPOS Invoice`")
+
+	def test_consolidation_round_off_error_2(self):
+		'''
+		Test the same case as above but with an Unpaid POS Invoice
+		'''
+		frappe.db.sql("delete from `tabPOS Invoice`")
+
+		try:
+			make_stock_entry(
+				to_warehouse="_Test Warehouse - _TC",
+				item_code="_Test Item",
+				rate=8000,
+				qty=10,
+			)
+
+			init_user_and_profile()
+
+			inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
+			inv.append("taxes", {
+				"account_head": "_Test Account VAT - _TC",
+				"charge_type": "On Net Total",
+				"cost_center": "_Test Cost Center - _TC",
+				"description": "VAT",
+				"doctype": "Sales Taxes and Charges",
+				"rate": 7.5,
+				"included_in_print_rate": 1
+			})
+			inv.append('payments', {
+				'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
+			})
+			inv.insert()
+			inv.submit()
+
+			inv2 = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
+			inv2.append("taxes", {
+				"account_head": "_Test Account VAT - _TC",
+				"charge_type": "On Net Total",
+				"cost_center": "_Test Cost Center - _TC",
+				"description": "VAT",
+				"doctype": "Sales Taxes and Charges",
+				"rate": 7.5,
+				"included_in_print_rate": 1
+			})
+			inv2.append('payments', {
+				'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
+			})
+			inv2.insert()
+			inv2.submit()
+
+			inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True)
+			inv3.append('payments', {
+				'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000
+			})
+			inv3.insert()
+			inv3.submit()
+
+			consolidate_pos_invoices()
+
+			inv.load_from_db()
+			consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
+			self.assertEqual(consolidated_invoice.outstanding_amount, 800)
+			self.assertNotEqual(consolidated_invoice.status, 'Paid')
+
+		finally:
+			frappe.set_user("Administrator")
+			frappe.db.sql("delete from `tabPOS Profile`")
+			frappe.db.sql("delete from `tabPOS Invoice`")
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index ac96b04..933fda8 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -249,13 +249,17 @@
 		"free_item_data": [],
 		"parent": args.parent,
 		"parenttype": args.parenttype,
-		"child_docname": args.get('child_docname')
+		"child_docname": args.get('child_docname'),
 	})
 
 	if args.ignore_pricing_rule or not args.item_code:
 		if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"):
-			item_details = remove_pricing_rule_for_item(args.get("pricing_rules"),
-				item_details, args.get('item_code'))
+			item_details = remove_pricing_rule_for_item(
+				args.get("pricing_rules"),
+				item_details,
+				item_code=args.get("item_code"),
+				rate=args.get("price_list_rate"),
+			)
 		return item_details
 
 	update_args_for_pricing_rule(args)
@@ -308,8 +312,12 @@
 		if not doc: return item_details
 
 	elif args.get("pricing_rules"):
-		item_details = remove_pricing_rule_for_item(args.get("pricing_rules"),
-			item_details, args.get('item_code'))
+		item_details = remove_pricing_rule_for_item(
+			args.get("pricing_rules"),
+			item_details,
+			item_code=args.get("item_code"),
+			rate=args.get("price_list_rate"),
+		)
 
 	return item_details
 
@@ -390,7 +398,7 @@
 			item_details[field] += (pricing_rule.get(field, 0)
 				if pricing_rule else args.get(field, 0))
 
-def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
+def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
 	from erpnext.accounts.doctype.pricing_rule.utils import (
 		get_applied_pricing_rules,
 		get_pricing_rule_items,
@@ -403,6 +411,7 @@
 			if pricing_rule.rate_or_discount == 'Discount Percentage':
 				item_details.discount_percentage = 0.0
 				item_details.discount_amount = 0.0
+				item_details.rate = rate or 0.0
 
 			if pricing_rule.rate_or_discount == 'Discount Amount':
 				item_details.discount_amount = 0.0
@@ -421,6 +430,7 @@
 			item_details.applied_on_items = ','.join(items)
 
 	item_details.pricing_rules = ''
+	item_details.pricing_rule_removed = True
 
 	return item_details
 
@@ -432,9 +442,12 @@
 	out = []
 	for item in item_list:
 		item = frappe._dict(item)
-		if item.get('pricing_rules'):
-			out.append(remove_pricing_rule_for_item(item.get("pricing_rules"),
-				item, item.item_code))
+		if item.get("pricing_rules"):
+			out.append(
+				remove_pricing_rule_for_item(
+					item.get("pricing_rules"), item, item.item_code, item.get("price_list_rate")
+				)
+			)
 
 	return out
 
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index 968137e..8338a5b0 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -628,6 +628,46 @@
 		for doc in [si, si1]:
 			doc.delete()
 
+	def test_remove_pricing_rule(self):
+		item = make_item("Water Flask")
+		make_item_price("Water Flask", "_Test Price List", 100)
+
+		pricing_rule_record = {
+			"doctype": "Pricing Rule",
+			"title": "_Test Water Flask Rule",
+			"apply_on": "Item Code",
+			"price_or_product_discount": "Price",
+			"items": [{
+				"item_code": "Water Flask",
+			}],
+			"selling": 1,
+			"currency": "INR",
+			"rate_or_discount": "Discount Percentage",
+			"discount_percentage": 20,
+			"company": "_Test Company"
+		}
+		rule = frappe.get_doc(pricing_rule_record)
+		rule.insert()
+
+		si = create_sales_invoice(do_not_save=True, item_code="Water Flask")
+		si.selling_price_list = "_Test Price List"
+		si.save()
+
+		self.assertEqual(si.items[0].price_list_rate, 100)
+		self.assertEqual(si.items[0].discount_percentage, 20)
+		self.assertEqual(si.items[0].rate, 80)
+
+		si.ignore_pricing_rule = 1
+		si.save()
+
+		self.assertEqual(si.items[0].discount_percentage, 0)
+		self.assertEqual(si.items[0].rate, 100)
+
+		si.delete()
+		rule.delete()
+		frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
+		item.delete()
+
 	def test_multiple_pricing_rules_with_min_qty(self):
 		make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
 			apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
@@ -648,6 +688,7 @@
 		frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
 		frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
 
+
 test_dependencies = ["Campaign"]
 
 def make_pricing_rule(**args):
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..b894f90 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -285,7 +285,7 @@
 				filters={ invoice_or_credit_note: self.name },
 				pluck="pos_closing_entry"
 			)
-			if pos_closing_entry:
+			if pos_closing_entry and pos_closing_entry[0]:
 				msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
 					frappe.bold("Consolidated Sales Invoice"),
 					get_link_to_form("POS Closing Entry", pos_closing_entry[0])
@@ -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/accounts/doctype/tax_category/tax_category.json b/erpnext/accounts/doctype/tax_category/tax_category.json
index f7145af..44a339f 100644
--- a/erpnext/accounts/doctype/tax_category/tax_category.json
+++ b/erpnext/accounts/doctype/tax_category/tax_category.json
@@ -2,12 +2,13 @@
  "actions": [],
  "allow_rename": 1,
  "autoname": "field:title",
- "creation": "2018-11-22 23:38:39.668804",
+ "creation": "2022-01-19 01:09:28.920486",
  "doctype": "DocType",
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
-  "title"
+  "title",
+  "disabled"
  ],
  "fields": [
   {
@@ -18,14 +19,21 @@
    "label": "Title",
    "reqd": 1,
    "unique": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "disabled",
+   "fieldtype": "Check",
+   "label": "Disabled"
   }
  ],
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2021-03-03 11:50:38.748872",
+ "modified": "2022-01-18 21:13:41.161017",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Tax Category",
+ "naming_rule": "By fieldname",
  "owner": "Administrator",
  "permissions": [
   {
@@ -65,5 +73,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 55bc967..d24d56b 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -319,13 +319,18 @@
 	"""
 
 	if not gl_entries:
-		gl_entries = frappe.get_all("GL Entry",
-			fields = ["*"],
-			filters = {
-				"voucher_type": voucher_type,
-				"voucher_no": voucher_no,
-				"is_cancelled": 0
-			})
+		gl_entry = frappe.qb.DocType("GL Entry")
+		gl_entries = (frappe.qb.from_(
+			gl_entry
+		).select(
+			'*'
+		).where(
+			gl_entry.voucher_type == voucher_type
+		).where(
+			gl_entry.voucher_no == voucher_no
+		).where(
+			gl_entry.is_cancelled == 0
+		).for_update()).run(as_dict=1)
 
 	if gl_entries:
 		validate_accounting_period(gl_entries)
@@ -333,23 +338,24 @@
 		set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no'])
 
 		for entry in gl_entries:
-			entry['name'] = None
-			debit = entry.get('debit', 0)
-			credit = entry.get('credit', 0)
+			new_gle = copy.deepcopy(entry)
+			new_gle['name'] = None
+			debit = new_gle.get('debit', 0)
+			credit = new_gle.get('credit', 0)
 
-			debit_in_account_currency = entry.get('debit_in_account_currency', 0)
-			credit_in_account_currency = entry.get('credit_in_account_currency', 0)
+			debit_in_account_currency = new_gle.get('debit_in_account_currency', 0)
+			credit_in_account_currency = new_gle.get('credit_in_account_currency', 0)
 
-			entry['debit'] = credit
-			entry['credit'] = debit
-			entry['debit_in_account_currency'] = credit_in_account_currency
-			entry['credit_in_account_currency'] = debit_in_account_currency
+			new_gle['debit'] = credit
+			new_gle['credit'] = debit
+			new_gle['debit_in_account_currency'] = credit_in_account_currency
+			new_gle['credit_in_account_currency'] = debit_in_account_currency
 
-			entry['remarks'] = "On cancellation of " + entry['voucher_no']
-			entry['is_cancelled'] = 1
+			new_gle['remarks'] = "On cancellation of " + new_gle['voucher_no']
+			new_gle['is_cancelled'] = 1
 
-			if entry['debit'] or entry['credit']:
-				make_entry(entry, adv_adj, "Yes")
+			if new_gle['debit'] or new_gle['credit']:
+				make_entry(new_gle, adv_adj, "Yes")
 
 
 def check_freezing_date(posting_date, adv_adj=False):
diff --git a/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json b/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json
deleted file mode 100644
index 1aa1c02..0000000
--- a/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "align_labels_right": 0,
- "creation": "2017-08-08 12:33:04.773099",
- "custom_format": 1,
- "disabled": 0,
- "doc_type": "Sales Invoice",
- "docstatus": 0,
- "doctype": "Print Format",
- "font": "Default",
- "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n    {{ letter_head }}\n{% endif %}\n<p class=\"text-center\">\n\t{{ doc.company }}<br>\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t<b>{{ _(\"GSTIN\") }}:</b>{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"<br>GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t<br>\n\t{% if doc.docstatus == 0 %}\n\t\t<b>{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}</b><br>\n\t{% else %}\n\t\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t{% endif %}\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t<b>{{ _(\"Customer\") }}:</b><br>\n\t\t{{ doc.customer_name }}<br>\n\t\t{{ customer_address }}\n\t{% endif %}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.gst_hsn_code -%}\n\t\t\t\t\t<br><b>{{ _(\"HSN/SAC\") }}:</b> {{ item.gst_hsn_code }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"Serial No\") }}:</b> {{ item.serial_no }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.rate }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t  {%- if (not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) and row.tax_amount != 0 -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ row.description }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t  {%- endif -%}\n\t\t{%- endfor -%}\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- if doc.change_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- endif -%}\n\t</tbody>\n</table>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
- "idx": 0,
- "line_breaks": 0,
- "modified": "2020-04-29 16:39:12.936215",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "GST POS Invoice",
- "owner": "Administrator",
- "print_format_builder": 0,
- "print_format_type": "Jinja",
- "raw_printing": 0,
- "show_section_headings": 0,
- "standard": "Yes"
-}
\ No newline at end of file
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js
index 685f2d6..2ba649d 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.js
+++ b/erpnext/accounts/report/gross_profit/gross_profit.js
@@ -42,6 +42,11 @@
 	"parent_field": "parent_invoice",
 	"initial_depth": 3,
 	"formatter": function(value, row, column, data, default_formatter) {
+		if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) {
+			column._options = "Sales Invoice";
+		} else {
+			column._options = "Item";
+		}
 		value = default_formatter(value, row, column, data);
 
 		if (data && (data.indent == 0.0 || row[1].content == "Total")) {
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index caee1a1..57f7974 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -23,7 +23,7 @@
 def get_result(filters, tds_docs, tds_accounts, tax_category_map):
 	supplier_map = get_supplier_pan_map()
 	tax_rate_map = get_tax_rate_map(filters)
-	gle_map = get_gle_map(filters, tds_docs)
+	gle_map = get_gle_map(tds_docs)
 
 	out = []
 	for name, details in gle_map.items():
@@ -78,7 +78,7 @@
 
 	return supplier_map
 
-def get_gle_map(filters, documents):
+def get_gle_map(documents):
 	# create gle_map of the form
 	# {"purchase_invoice": list of dict of all gle created for this invoice}
 	gle_map = {}
@@ -86,7 +86,7 @@
 	gle = frappe.db.get_all('GL Entry',
 		{
 			"voucher_no": ["in", documents],
-			"credit": (">", 0)
+			"is_cancelled": 0
 		},
 		["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
 	)
@@ -184,21 +184,28 @@
 	payment_entries = []
 	journal_entries = []
 	tax_category_map = {}
+	or_filters = {}
+	bank_accounts = frappe.get_all('Account', {'is_group': 0, 'account_type': 'Bank'}, pluck="name")
 
 	tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')},
 		pluck="account")
 
 	query_filters = {
-		"credit": ('>', 0),
 		"account": ("in", tds_accounts),
 		"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
-		"is_cancelled": 0
+		"is_cancelled": 0,
+		"against": ("not in", bank_accounts)
 	}
 
-	if filters.get('supplier'):
-		query_filters.update({'against': filters.get('supplier')})
+	if filters.get("supplier"):
+		del query_filters["account"]
+		del query_filters["against"]
+		or_filters = {
+			"against": filters.get('supplier'),
+			"party": filters.get('supplier')
+		}
 
-	tds_docs = frappe.get_all("GL Entry", query_filters, ["voucher_no", "voucher_type", "against", "party"])
+	tds_docs = frappe.get_all("GL Entry", filters=query_filters, or_filters=or_filters, fields=["voucher_no", "voucher_type", "against", "party"])
 
 	for d in tds_docs:
 		if d.voucher_type == "Purchase Invoice":
diff --git a/erpnext/accounts/print_format/gst_pos_invoice/__init__.py b/erpnext/bulk_transaction/__init__.py
similarity index 100%
copy from erpnext/accounts/print_format/gst_pos_invoice/__init__.py
copy to erpnext/bulk_transaction/__init__.py
diff --git a/erpnext/accounts/print_format/gst_pos_invoice/__init__.py b/erpnext/bulk_transaction/doctype/__init__.py
similarity index 100%
rename from erpnext/accounts/print_format/gst_pos_invoice/__init__.py
rename to erpnext/bulk_transaction/doctype/__init__.py
diff --git a/erpnext/accounts/print_format/gst_pos_invoice/__init__.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/__init__.py
similarity index 100%
copy from erpnext/accounts/print_format/gst_pos_invoice/__init__.py
copy to 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/accounts/print_format/gst_pos_invoice/__init__.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/__init__.py
similarity index 100%
copy from erpnext/accounts/print_format/gst_pos_invoice/__init__.py
copy to 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/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index b828a43..50321ba 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -6,14 +6,17 @@
  "document_type": "Other",
  "engine": "InnoDB",
  "field_order": [
+  "supplier_and_price_defaults_section",
   "supp_master_name",
   "supplier_group",
+  "column_break_4",
   "buying_price_list",
   "maintain_same_rate_action",
   "role_to_override_stop_action",
-  "column_break_3",
+  "transaction_settings_section",
   "po_required",
   "pr_required",
+  "column_break_12",
   "maintain_same_rate",
   "allow_multiple_items",
   "bill_for_rejected_quantity_in_purchase_invoice",
@@ -43,10 +46,6 @@
    "options": "Price List"
   },
   {
-   "fieldname": "column_break_3",
-   "fieldtype": "Column Break"
-  },
-  {
    "fieldname": "po_required",
    "fieldtype": "Select",
    "label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
@@ -73,7 +72,7 @@
   {
    "fieldname": "subcontract",
    "fieldtype": "Section Break",
-   "label": "Subcontract"
+   "label": "Subcontracting Settings"
   },
   {
    "default": "Material Transferred for Subcontract",
@@ -116,6 +115,24 @@
    "fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
    "fieldtype": "Check",
    "label": "Bill for Rejected Quantity in Purchase Invoice"
+  },
+  {
+   "fieldname": "supplier_and_price_defaults_section",
+   "fieldtype": "Section Break",
+   "label": "Supplier and Price Defaults"
+  },
+  {
+   "fieldname": "column_break_4",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "transaction_settings_section",
+   "fieldtype": "Section Break",
+   "label": "Transaction Settings"
+  },
+  {
+   "fieldname": "column_break_12",
+   "fieldtype": "Column Break"
   }
  ],
  "icon": "fa fa-cog",
@@ -123,7 +140,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-09-08 19:26:23.548837",
+ "modified": "2022-01-27 17:57:58.367048",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Buying Settings",
@@ -141,5 +158,6 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
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/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 9a63afc..645e97e 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -682,17 +682,18 @@
 
 		bin1 = frappe.db.get_value("Bin",
 			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
-			fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1)
+			fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1)
 
 		# Submit PO
 		po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
 
 		bin2 = frappe.db.get_value("Bin",
 			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
-			fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1)
+			fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1)
 
 		self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
 		self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
+		self.assertNotEqual(bin1.modified, bin2.modified)
 
 		# Create stock transfer
 		rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item",
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/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
index 49e3351..7908c35 100644
--- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
+++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
@@ -49,7 +49,7 @@
 				"min_grade":0.0,"name":"Very Poor",
 				"prevent_rfqs":1,
 				"notify_supplier":0,
-				"doctype":"Supplier Scorecard Standing",
+				"doctype":"Supplier Scorecard Scoring Standing",
 				"max_grade":30.0,
 				"prevent_pos":1,
 				"warn_pos":0,
@@ -65,7 +65,7 @@
 				"name":"Poor",
 				"prevent_rfqs":1,
 				"notify_supplier":0,
-				"doctype":"Supplier Scorecard Standing",
+				"doctype":"Supplier Scorecard Scoring Standing",
 				"max_grade":50.0,
 				"prevent_pos":0,
 				"warn_pos":0,
@@ -81,7 +81,7 @@
 				"name":"Average",
 				"prevent_rfqs":0,
 				"notify_supplier":0,
-				"doctype":"Supplier Scorecard Standing",
+				"doctype":"Supplier Scorecard Scoring Standing",
 				"max_grade":80.0,
 				"prevent_pos":0,
 				"warn_pos":0,
@@ -97,7 +97,7 @@
 				"name":"Excellent",
 				"prevent_rfqs":0,
 				"notify_supplier":0,
-				"doctype":"Supplier Scorecard Standing",
+				"doctype":"Supplier Scorecard Scoring Standing",
 				"max_grade":100.0,
 				"prevent_pos":0,
 				"warn_pos":0,
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index bab16a4..994b903 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -407,6 +407,22 @@
 								if item_qty != len(get_serial_nos(item.get('serial_no'))):
 									item.set(fieldname, value)
 
+							elif (
+								ret.get("pricing_rule_removed")
+								and value is not None
+								and fieldname
+								in [
+									"discount_percentage",
+									"discount_amount",
+									"rate",
+									"margin_rate_or_amount",
+									"margin_type",
+									"remove_free_item",
+								]
+							):
+								# reset pricing rule fields if pricing_rule_removed
+								item.set(fieldname, value)
+
 					if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'):
 						item.set('is_fixed_asset', ret.get('is_fixed_asset', 0))
 
@@ -1318,6 +1334,9 @@
 				payment_schedule['discount_type'] = schedule.discount_type
 				payment_schedule['discount'] = schedule.discount
 
+			if not schedule.invoice_portion:
+				payment_schedule['payment_amount'] = schedule.payment_amount
+
 			self.append("payment_schedule", payment_schedule)
 
 	def set_due_date(self):
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 902e115..dd9b45c 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -710,6 +710,7 @@
 
 	item_doc = frappe.get_cached_doc('Item', filters.get('item_code'))
 	item_group = filters.get('item_group')
+	company = filters.get('company')
 	taxes = item_doc.taxes or []
 
 	while item_group:
@@ -718,7 +719,7 @@
 		item_group = item_group_doc.parent_item_group
 
 	if not taxes:
-		return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """)
+		return frappe.get_all('Item Tax Template', filters={'disabled': 0, 'company': company}, as_list=True)
 	else:
 		valid_from = filters.get('valid_from')
 		valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
@@ -727,7 +728,7 @@
 			'item_code': filters.get('item_code'),
 			'posting_date': valid_from,
 			'tax_category': filters.get('tax_category'),
-			'company': filters.get('company')
+			'company': company
 		}
 
 		taxes = _get_item_tax_template(args, taxes, for_validate=True)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 75fcaee..31b2209 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -74,7 +74,8 @@
 				doctype=self.doctype, company=self.company,
 				posting_date=self.get('posting_date'),
 				fetch_payment_terms_template=fetch_payment_terms_template,
-				party_address=self.customer_address, shipping_address=self.shipping_address_name)
+				party_address=self.customer_address, shipping_address=self.shipping_address_name,
+				company_address=self.get('company_address'))
 			if not self.meta.get_field("sales_team"):
 				party_details.pop("sales_team")
 			self.update_if_missing(party_details)
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/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 075e3e3..2776628 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -106,6 +106,9 @@
 		self.doc.conversion_rate = flt(self.doc.conversion_rate)
 
 	def calculate_item_values(self):
+		if self.doc.get('is_consolidated'):
+			return
+
 		if not self.discount_amount_applied:
 			for item in self.doc.get("items"):
 				self.doc.round_floats_in(item)
@@ -647,12 +650,12 @@
 	def calculate_change_amount(self):
 		self.doc.change_amount = 0.0
 		self.doc.base_change_amount = 0.0
+		grand_total = self.doc.rounded_total or self.doc.grand_total
+		base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
 
 		if self.doc.doctype == "Sales Invoice" \
-			and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \
+			and self.doc.paid_amount > grand_total and not self.doc.is_return \
 			and any(d.type == "Cash" for d in self.doc.payments):
-			grand_total = self.doc.rounded_total or self.doc.grand_total
-			base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
 
 			self.doc.change_amount = flt(self.doc.paid_amount - grand_total +
 				self.doc.write_off_amount, self.doc.precision("change_amount"))
diff --git a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json
index 8a8d425..0cfcf0e 100644
--- a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json
+++ b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json
@@ -3,7 +3,7 @@
  "allow_events_in_timeline": 0, 
  "allow_guest_to_view": 0, 
  "allow_import": 0, 
- "allow_rename": 0, 
+ "allow_rename": 1, 
  "autoname": "field:lost_reason", 
  "beta": 0, 
  "creation": "2018-12-28 14:48:51.044975", 
@@ -57,7 +57,7 @@
  "issingle": 0, 
  "istable": 0, 
  "max_attachments": 0, 
- "modified": "2018-12-28 14:49:43.336437", 
+ "modified": "2022-02-16 10:49:43.336437", 
  "modified_by": "Administrator", 
  "module": "CRM", 
  "name": "Opportunity Lost Reason", 
@@ -150,4 +150,4 @@
  "track_changes": 0, 
  "track_seen": 0, 
  "track_views": 0
-}
\ No newline at end of file
+}
diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py
index bb6b3ef..3107c01 100644
--- a/erpnext/e_commerce/variant_selector/item_variants_cache.py
+++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py
@@ -66,26 +66,24 @@
 			)
 		]
 
-		# join with Website Item
-		item_variants_data = frappe.get_all(
-			'Item Variant Attribute',
-			{'variant_of': parent_item_code},
-			['parent', 'attribute', 'attribute_value'],
-			order_by='name',
-			as_list=1
+		# Get Variants and tehir Attributes that are not disabled
+		iva = frappe.qb.DocType("Item Variant Attribute")
+		item = frappe.qb.DocType("Item")
+		query = (
+			frappe.qb.from_(iva)
+			.join(item).on(item.name == iva.parent)
+			.select(
+				iva.parent, iva.attribute, iva.attribute_value
+			).where(
+				(iva.variant_of == parent_item_code)
+				& (item.disabled == 0)
+			).orderby(iva.name)
 		)
-
-		disabled_items = set(
-			[i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
-		)
+		item_variants_data = query.run()
 
 		attribute_value_item_map = frappe._dict()
 		item_attribute_value_map = frappe._dict()
 
-		# dont consider variants that are disabled
-		# pull all other variants
-		item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
-
 		for row in item_variants_data:
 			item_code, attribute, attribute_value = row
 			# (attr, value) => [item1, item2]
@@ -124,4 +122,7 @@
 def enqueue_build_cache(item_code):
 	if frappe.cache().hget('item_cache_build_in_progress', item_code):
 		return
-	frappe.enqueue(build_cache, item_code=item_code, queue='long')
+	frappe.enqueue(
+		"erpnext.e_commerce.variant_selector.item_variants_cache.build_cache",
+		item_code=item_code, queue='long'
+	)
diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py
index b83961e..4d907c6 100644
--- a/erpnext/e_commerce/variant_selector/test_variant_selector.py
+++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py
@@ -104,6 +104,8 @@
 		})
 
 		make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
+
+		frappe.local.shopping_cart_settings = None # clear cached settings values
 		next_values = get_next_attribute_and_values(
 			"Test-Tshirt-Temp",
 			selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/__init__.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/__init__.py
+++ /dev/null
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
deleted file mode 100644
index 29bc36f..0000000
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
+++ /dev/null
@@ -1,524 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-
-import csv
-import math
-import time
-from io import StringIO
-
-import dateutil
-import frappe
-from frappe import _
-
-import erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_api as mws
-
-
-#Get and Create Products
-def get_products_details():
-	products = get_products_instance()
-	reports = get_reports_instance()
-
-	mws_settings = frappe.get_doc("Amazon MWS Settings")
-	market_place_list = return_as_list(mws_settings.market_place_id)
-
-	for marketplace in market_place_list:
-		report_id = request_and_fetch_report_id("_GET_FLAT_FILE_OPEN_LISTINGS_DATA_", None, None, market_place_list)
-
-		if report_id:
-			listings_response = reports.get_report(report_id=report_id)
-
-			#Get ASIN Codes
-			string_io = StringIO(frappe.safe_decode(listings_response.original))
-			csv_rows = list(csv.reader(string_io, delimiter='\t'))
-			asin_list = list(set([row[1] for row in csv_rows[1:]]))
-			#break into chunks of 10
-			asin_chunked_list = list(chunks(asin_list, 10))
-
-			#Map ASIN Codes to SKUs
-			sku_asin = [{"asin":row[1],"sku":row[0]} for row in csv_rows[1:]]
-
-			#Fetch Products List from ASIN
-			for asin_list in asin_chunked_list:
-				products_response = call_mws_method(products.get_matching_product,marketplaceid=marketplace,
-					asins=asin_list)
-
-				matching_products_list = products_response.parsed
-				for product in matching_products_list:
-					skus = [row["sku"] for row in sku_asin if row["asin"]==product.ASIN]
-					for sku in skus:
-						create_item_code(product, sku)
-
-def get_products_instance():
-	mws_settings = frappe.get_doc("Amazon MWS Settings")
-	products = mws.Products(
-			account_id = mws_settings.seller_id,
-			access_key = mws_settings.aws_access_key_id,
-			secret_key = mws_settings.secret_key,
-			region = mws_settings.region,
-			domain = mws_settings.domain
-			)
-
-	return products
-
-def get_reports_instance():
-	mws_settings = frappe.get_doc("Amazon MWS Settings")
-	reports = mws.Reports(
-			account_id = mws_settings.seller_id,
-			access_key = mws_settings.aws_access_key_id,
-			secret_key = mws_settings.secret_key,
-			region = mws_settings.region,
-			domain = mws_settings.domain
-	)
-
-	return reports
-
-#returns list as expected by amazon API
-def return_as_list(input_value):
-	if isinstance(input_value, list):
-		return input_value
-	else:
-		return [input_value]
-
-#function to chunk product data
-def chunks(l, n):
-	for i in range(0, len(l), n):
-		yield l[i:i+n]
-
-def request_and_fetch_report_id(report_type, start_date=None, end_date=None, marketplaceids=None):
-	reports = get_reports_instance()
-	report_response = reports.request_report(report_type=report_type,
-			start_date=start_date,
-			end_date=end_date,
-			marketplaceids=marketplaceids)
-
-	report_request_id = report_response.parsed["ReportRequestInfo"]["ReportRequestId"]["value"]
-	generated_report_id = None
-	#poll to get generated report
-	for x in range(1,10):
-		report_request_list_response = reports.get_report_request_list(requestids=[report_request_id])
-		report_status = report_request_list_response.parsed["ReportRequestInfo"]["ReportProcessingStatus"]["value"]
-
-		if report_status == "_SUBMITTED_" or report_status == "_IN_PROGRESS_":
-			#add time delay to wait for amazon to generate report
-			time.sleep(15)
-			continue
-		elif report_status == "_CANCELLED_":
-			break
-		elif report_status == "_DONE_NO_DATA_":
-			break
-		elif report_status == "_DONE_":
-			generated_report_id =  report_request_list_response.parsed["ReportRequestInfo"]["GeneratedReportId"]["value"]
-			break
-	return generated_report_id
-
-def call_mws_method(mws_method, *args, **kwargs):
-
-	mws_settings = frappe.get_doc("Amazon MWS Settings")
-	max_retries = mws_settings.max_retry_limit
-
-	for x in range(0, max_retries):
-		try:
-			response = mws_method(*args, **kwargs)
-			return response
-		except Exception as e:
-			delay = math.pow(4, x) * 125
-			frappe.log_error(message=e, title=f'Method "{mws_method.__name__}" failed')
-			time.sleep(delay)
-			continue
-
-	mws_settings.enable_sync = 0
-	mws_settings.save()
-
-	frappe.throw(_("Sync has been temporarily disabled because maximum retries have been exceeded"))
-
-def create_item_code(amazon_item_json, sku):
-	if frappe.db.get_value("Item", sku):
-		return
-
-	item = frappe.new_doc("Item")
-
-	new_manufacturer = create_manufacturer(amazon_item_json)
-	new_brand = create_brand(amazon_item_json)
-
-	mws_settings = frappe.get_doc("Amazon MWS Settings")
-
-	item.item_code = sku
-	item.amazon_item_code = amazon_item_json.ASIN
-	item.item_group = mws_settings.item_group
-	item.description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
-	item.brand = new_brand
-	item.manufacturer = new_manufacturer
-
-	item.image = amazon_item_json.Product.AttributeSets.ItemAttributes.SmallImage.URL
-
-	temp_item_group = amazon_item_json.Product.AttributeSets.ItemAttributes.ProductGroup
-
-	item_group = frappe.db.get_value("Item Group",filters={"item_group_name": temp_item_group})
-
-	if not item_group:
-		igroup = frappe.new_doc("Item Group")
-		igroup.item_group_name = temp_item_group
-		igroup.parent_item_group =  mws_settings.item_group
-		igroup.insert()
-
-	item.append("item_defaults", {'company':mws_settings.company})
-
-	item.insert(ignore_permissions=True)
-	create_item_price(amazon_item_json, item.item_code)
-
-	return item.name
-
-def create_manufacturer(amazon_item_json):
-	if not amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer:
-		return None
-
-	existing_manufacturer = frappe.db.get_value("Manufacturer",
-		filters={"short_name":amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer})
-
-	if not existing_manufacturer:
-		manufacturer = frappe.new_doc("Manufacturer")
-		manufacturer.short_name = amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer
-		manufacturer.insert()
-		return manufacturer.short_name
-	else:
-		return existing_manufacturer
-
-def create_brand(amazon_item_json):
-	if not amazon_item_json.Product.AttributeSets.ItemAttributes.Brand:
-		return None
-
-	existing_brand = frappe.db.get_value("Brand",
-		filters={"brand":amazon_item_json.Product.AttributeSets.ItemAttributes.Brand})
-	if not existing_brand:
-		brand = frappe.new_doc("Brand")
-		brand.brand = amazon_item_json.Product.AttributeSets.ItemAttributes.Brand
-		brand.insert()
-		return brand.brand
-	else:
-		return existing_brand
-
-def create_item_price(amazon_item_json, item_code):
-	item_price = frappe.new_doc("Item Price")
-	item_price.price_list = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "price_list")
-	if not("ListPrice" in amazon_item_json.Product.AttributeSets.ItemAttributes):
-		item_price.price_list_rate = 0
-	else:
-		item_price.price_list_rate = amazon_item_json.Product.AttributeSets.ItemAttributes.ListPrice.Amount
-
-	item_price.item_code = item_code
-	item_price.insert()
-
-#Get and create Orders
-def get_orders(after_date):
-	try:
-		orders = get_orders_instance()
-		statuses = ["PartiallyShipped", "Unshipped", "Shipped", "Canceled"]
-		mws_settings = frappe.get_doc("Amazon MWS Settings")
-		market_place_list = return_as_list(mws_settings.market_place_id)
-
-		orders_response = call_mws_method(orders.list_orders, marketplaceids=market_place_list,
-			fulfillment_channels=["MFN", "AFN"],
-			lastupdatedafter=after_date,
-			orderstatus=statuses,
-			max_results='50')
-
-		while True:
-			orders_list = []
-
-			if "Order" in orders_response.parsed.Orders:
-				orders_list = return_as_list(orders_response.parsed.Orders.Order)
-
-			if len(orders_list) == 0:
-				break
-
-			for order in orders_list:
-				create_sales_order(order, after_date)
-
-			if not "NextToken" in orders_response.parsed:
-				break
-
-			next_token = orders_response.parsed.NextToken
-			orders_response = call_mws_method(orders.list_orders_by_next_token, next_token)
-
-	except Exception as e:
-		frappe.log_error(title="get_orders", message=e)
-
-def get_orders_instance():
-	mws_settings = frappe.get_doc("Amazon MWS Settings")
-	orders = mws.Orders(
-			account_id = mws_settings.seller_id,
-			access_key = mws_settings.aws_access_key_id,
-			secret_key = mws_settings.secret_key,
-			region= mws_settings.region,
-			domain= mws_settings.domain,
-			version="2013-09-01"
-		)
-
-	return orders
-
-def create_sales_order(order_json,after_date):
-	customer_name = create_customer(order_json)
-	create_address(order_json, customer_name)
-
-	market_place_order_id = order_json.AmazonOrderId
-
-	so = frappe.db.get_value("Sales Order",
-			filters={"amazon_order_id": market_place_order_id},
-			fieldname="name")
-
-	taxes_and_charges = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "taxes_charges")
-
-	if so:
-		return
-
-	if not so:
-		items = get_order_items(market_place_order_id)
-		delivery_date = dateutil.parser.parse(order_json.LatestShipDate).strftime("%Y-%m-%d")
-		transaction_date = dateutil.parser.parse(order_json.PurchaseDate).strftime("%Y-%m-%d")
-
-		so = frappe.get_doc({
-				"doctype": "Sales Order",
-				"naming_series": "SO-",
-				"amazon_order_id": market_place_order_id,
-				"marketplace_id": order_json.MarketplaceId,
-				"customer": customer_name,
-				"delivery_date": delivery_date,
-				"transaction_date": transaction_date,
-				"items": items,
-				"company": frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "company")
-			})
-
-		try:
-			if taxes_and_charges:
-				charges_and_fees = get_charges_and_fees(market_place_order_id)
-				for charge in charges_and_fees.get("charges"):
-					so.append('taxes', charge)
-
-				for fee in charges_and_fees.get("fees"):
-					so.append('taxes', fee)
-
-			so.insert(ignore_permissions=True)
-			so.submit()
-
-		except Exception as e:
-			import traceback
-			frappe.log_error(message=traceback.format_exc(), title="Create Sales Order")
-
-def create_customer(order_json):
-	order_customer_name = ""
-
-	if not("BuyerName" in order_json):
-		order_customer_name = "Buyer - " + order_json.AmazonOrderId
-	else:
-		order_customer_name = order_json.BuyerName
-
-	existing_customer_name = frappe.db.get_value("Customer",
-			filters={"name": order_customer_name}, fieldname="name")
-
-	if existing_customer_name:
-		filters = [
-				["Dynamic Link", "link_doctype", "=", "Customer"],
-				["Dynamic Link", "link_name", "=", existing_customer_name],
-				["Dynamic Link", "parenttype", "=", "Contact"]
-			]
-
-		existing_contacts = frappe.get_list("Contact", filters)
-
-		if existing_contacts:
-			pass
-		else:
-			new_contact = frappe.new_doc("Contact")
-			new_contact.first_name = order_customer_name
-			new_contact.append('links', {
-				"link_doctype": "Customer",
-				"link_name": existing_customer_name
-			})
-			new_contact.insert()
-
-		return existing_customer_name
-	else:
-		mws_customer_settings = frappe.get_doc("Amazon MWS Settings")
-		new_customer = frappe.new_doc("Customer")
-		new_customer.customer_name = order_customer_name
-		new_customer.customer_group = mws_customer_settings.customer_group
-		new_customer.territory = mws_customer_settings.territory
-		new_customer.customer_type = mws_customer_settings.customer_type
-		new_customer.save()
-
-		new_contact = frappe.new_doc("Contact")
-		new_contact.first_name = order_customer_name
-		new_contact.append('links', {
-			"link_doctype": "Customer",
-			"link_name": new_customer.name
-		})
-
-		new_contact.insert()
-
-		return new_customer.name
-
-def create_address(amazon_order_item_json, customer_name):
-
-	filters = [
-			["Dynamic Link", "link_doctype", "=", "Customer"],
-			["Dynamic Link", "link_name", "=", customer_name],
-			["Dynamic Link", "parenttype", "=", "Address"]
-		]
-
-	existing_address = frappe.get_list("Address", filters)
-
-	if not("ShippingAddress" in amazon_order_item_json):
-		return None
-	else:
-		make_address = frappe.new_doc("Address")
-
-		if "AddressLine1" in amazon_order_item_json.ShippingAddress:
-			make_address.address_line1 = amazon_order_item_json.ShippingAddress.AddressLine1
-		else:
-			make_address.address_line1 = "Not Provided"
-
-		if "City" in amazon_order_item_json.ShippingAddress:
-			make_address.city = amazon_order_item_json.ShippingAddress.City
-		else:
-			make_address.city = "Not Provided"
-
-		if "StateOrRegion" in amazon_order_item_json.ShippingAddress:
-			make_address.state = amazon_order_item_json.ShippingAddress.StateOrRegion
-
-		if "PostalCode" in amazon_order_item_json.ShippingAddress:
-			make_address.pincode = amazon_order_item_json.ShippingAddress.PostalCode
-
-		for address in existing_address:
-			address_doc = frappe.get_doc("Address", address["name"])
-			if (address_doc.address_line1 == make_address.address_line1 and
-				address_doc.pincode == make_address.pincode):
-				return address
-
-		make_address.append("links", {
-			"link_doctype": "Customer",
-			"link_name": customer_name
-		})
-		make_address.address_type = "Shipping"
-		make_address.insert()
-
-def get_order_items(market_place_order_id):
-	mws_orders = get_orders_instance()
-
-	order_items_response = call_mws_method(mws_orders.list_order_items, amazon_order_id=market_place_order_id)
-	final_order_items = []
-
-	order_items_list = return_as_list(order_items_response.parsed.OrderItems.OrderItem)
-
-	warehouse = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "warehouse")
-
-	while True:
-		for order_item in order_items_list:
-
-			if not "ItemPrice" in order_item:
-				price = 0
-			else:
-				price = order_item.ItemPrice.Amount
-
-			final_order_items.append({
-				"item_code": get_item_code(order_item),
-				"item_name": order_item.SellerSKU,
-				"description": order_item.Title,
-				"rate": price,
-				"qty": order_item.QuantityOrdered,
-				"stock_uom": "Nos",
-				"warehouse": warehouse,
-				"conversion_factor": "1.0"
-			})
-
-		if not "NextToken" in order_items_response.parsed:
-			break
-
-		next_token = order_items_response.parsed.NextToken
-
-		order_items_response = call_mws_method(mws_orders.list_order_items_by_next_token, next_token)
-		order_items_list = return_as_list(order_items_response.parsed.OrderItems.OrderItem)
-
-	return final_order_items
-
-def get_item_code(order_item):
-	sku = order_item.SellerSKU
-	item_code = frappe.db.get_value("Item", {"item_code": sku}, "item_code")
-	if item_code:
-		return item_code
-
-def get_charges_and_fees(market_place_order_id):
-	finances = get_finances_instance()
-
-	charges_fees = {"charges":[], "fees":[]}
-
-	response = call_mws_method(finances.list_financial_events, amazon_order_id=market_place_order_id)
-
-	shipment_event_list = return_as_list(response.parsed.FinancialEvents.ShipmentEventList)
-
-	for shipment_event in shipment_event_list:
-		if shipment_event:
-			shipment_item_list = return_as_list(shipment_event.ShipmentEvent.ShipmentItemList.ShipmentItem)
-
-			for shipment_item in shipment_item_list:
-				charges, fees = [], []
-
-				if 'ItemChargeList' in shipment_item.keys():
-					charges = return_as_list(shipment_item.ItemChargeList.ChargeComponent)
-
-				if 'ItemFeeList' in shipment_item.keys():
-					fees = return_as_list(shipment_item.ItemFeeList.FeeComponent)
-
-				for charge in charges:
-					if(charge.ChargeType != "Principal") and float(charge.ChargeAmount.CurrencyAmount) != 0:
-						charge_account = get_account(charge.ChargeType)
-						charges_fees.get("charges").append({
-							"charge_type":"Actual",
-							"account_head": charge_account,
-							"tax_amount": charge.ChargeAmount.CurrencyAmount,
-							"description": charge.ChargeType + " for " + shipment_item.SellerSKU
-							})
-
-				for fee in fees:
-					if float(fee.FeeAmount.CurrencyAmount) != 0:
-						fee_account = get_account(fee.FeeType)
-						charges_fees.get("fees").append({
-							"charge_type":"Actual",
-							"account_head": fee_account,
-							"tax_amount": fee.FeeAmount.CurrencyAmount,
-							"description": fee.FeeType + " for " + shipment_item.SellerSKU
-							})
-
-	return charges_fees
-
-def get_finances_instance():
-
-	mws_settings = frappe.get_doc("Amazon MWS Settings")
-
-	finances = mws.Finances(
-			account_id = mws_settings.seller_id,
-			access_key = mws_settings.aws_access_key_id,
-			secret_key = mws_settings.secret_key,
-			region= mws_settings.region,
-			domain= mws_settings.domain,
-			version="2015-05-01"
-		)
-
-	return finances
-
-def get_account(name):
-	existing_account = frappe.db.get_value("Account", {"account_name": "Amazon {0}".format(name)})
-	account_name = existing_account
-	mws_settings = frappe.get_doc("Amazon MWS Settings")
-
-	if not existing_account:
-		try:
-			new_account = frappe.new_doc("Account")
-			new_account.account_name = "Amazon {0}".format(name)
-			new_account.company = mws_settings.company
-			new_account.parent_account = mws_settings.market_place_account_group
-			new_account.insert(ignore_permissions=True)
-			account_name = new_account.name
-		except Exception as e:
-			frappe.log_error(message=e, title="Create Account")
-
-	return account_name
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py
deleted file mode 100755
index 4caf137..0000000
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py
+++ /dev/null
@@ -1,651 +0,0 @@
-#!/usr/bin/env python
-#
-# Basic interface to Amazon MWS
-# Based on http://code.google.com/p/amazon-mws-python
-# Extended to include finances object
-
-import base64
-import hashlib
-import hmac
-import re
-from urllib.parse import quote
-
-from erpnext.erpnext_integrations.doctype.amazon_mws_settings import xml_utils
-
-try:
-	from xml.etree.ElementTree import ParseError as XMLError
-except ImportError:
-	from xml.parsers.expat import ExpatError as XMLError
-
-from time import gmtime, strftime
-
-from requests import request
-from requests.exceptions import HTTPError
-
-__all__ = [
-	'Feeds',
-	'Inventory',
-	'MWSError',
-	'Reports',
-	'Orders',
-	'Products',
-	'Recommendations',
-	'Sellers',
-	'Finances'
-]
-
-# See https://images-na.ssl-images-amazon.com/images/G/01/mwsportal/doc/en_US/bde/MWSDeveloperGuide._V357736853_.pdf page 8
-# for a list of the end points and marketplace IDs
-
-MARKETPLACES = {
-	"CA": "https://mws.amazonservices.ca", #A2EUQ1WTGCTBG2
-	"US": "https://mws.amazonservices.com", #ATVPDKIKX0DER",
-	"DE": "https://mws-eu.amazonservices.com", #A1PA6795UKMFR9
-	"ES": "https://mws-eu.amazonservices.com", #A1RKKUPIHCS9HS
-	"FR": "https://mws-eu.amazonservices.com", #A13V1IB3VIYZZH
-	"IN": "https://mws.amazonservices.in", #A21TJRUUN4KGV
-	"IT": "https://mws-eu.amazonservices.com", #APJ6JRA9NG5V4
-	"UK": "https://mws-eu.amazonservices.com", #A1F83G8C2ARO7P
-	"JP": "https://mws.amazonservices.jp", #A1VC38T7YXB528
-	"CN": "https://mws.amazonservices.com.cn", #AAHKV2X7AFYLW
-	"AE": "	https://mws.amazonservices.ae", #A2VIGQ35RCS4UG
-	"MX": "https://mws.amazonservices.com.mx", #A1AM78C64UM0Y8
-	"BR": "https://mws.amazonservices.com", #A2Q3Y263D00KWC
-}
-
-
-class MWSError(Exception):
-	"""
-		Main MWS Exception class
-	"""
-	# Allows quick access to the response object.
-	# Do not rely on this attribute, always check if its not None.
-	response = None
-
-def calc_md5(string):
-	"""Calculates the MD5 encryption for the given string
-	"""
-	md = hashlib.md5()
-	md.update(string)
-	return base64.encodebytes(md.digest()).decode().strip()
-
-
-
-def remove_empty(d):
-	"""
-		Helper function that removes all keys from a dictionary (d),
-	that have an empty value.
-	"""
-	for key in list(d):
-		if not d[key]:
-			del d[key]
-	return d
-
-def remove_namespace(xml):
-	xml = xml.decode('utf-8')
-	regex = re.compile(' xmlns(:ns2)?="[^"]+"|(ns2:)|(xml:)')
-	return regex.sub('', xml)
-
-class DictWrapper(object):
-	def __init__(self, xml, rootkey=None):
-		self.original = xml
-		self._rootkey = rootkey
-		self._mydict = xml_utils.xml2dict().fromstring(remove_namespace(xml))
-		self._response_dict = self._mydict.get(list(self._mydict)[0], self._mydict)
-
-	@property
-	def parsed(self):
-		if self._rootkey:
-			return self._response_dict.get(self._rootkey)
-		else:
-			return self._response_dict
-
-class DataWrapper(object):
-	"""
-		Text wrapper in charge of validating the hash sent by Amazon.
-	"""
-	def __init__(self, data, header):
-		self.original = data
-		if 'content-md5' in header:
-			hash_ = calc_md5(self.original)
-			if header['content-md5'] != hash_:
-				raise MWSError("Wrong Contentlength, maybe amazon error...")
-
-	@property
-	def parsed(self):
-		return self.original
-
-class MWS(object):
-	""" Base Amazon API class """
-
-	# This is used to post/get to the different uris used by amazon per api
-	# ie. /Orders/2011-01-01
-	# All subclasses must define their own URI only if needed
-	URI = "/"
-
-	# The API version varies in most amazon APIs
-	VERSION = "2009-01-01"
-
-	# There seem to be some xml namespace issues. therefore every api subclass
-	# is recommended to define its namespace, so that it can be referenced
-	# like so AmazonAPISubclass.NS.
-	# For more information see http://stackoverflow.com/a/8719461/389453
-	NS = ''
-
-	# Some APIs are available only to either a "Merchant" or "Seller"
-	# the type of account needs to be sent in every call to the amazon MWS.
-	# This constant defines the exact name of the parameter Amazon expects
-	# for the specific API being used.
-	# All subclasses need to define this if they require another account type
-	# like "Merchant" in which case you define it like so.
-	# ACCOUNT_TYPE = "Merchant"
-	# Which is the name of the parameter for that specific account type.
-	ACCOUNT_TYPE = "SellerId"
-
-	def __init__(self, access_key, secret_key, account_id, region='US', domain='', uri="", version=""):
-		self.access_key = access_key
-		self.secret_key = secret_key
-		self.account_id = account_id
-		self.version = version or self.VERSION
-		self.uri = uri or self.URI
-
-		if domain:
-			self.domain = domain
-		elif region in MARKETPLACES:
-			self.domain = MARKETPLACES[region]
-		else:
-			error_msg = "Incorrect region supplied ('%(region)s'). Must be one of the following: %(marketplaces)s" % {
-				"marketplaces" : ', '.join(MARKETPLACES.keys()),
-				"region" : region,
-			}
-			raise MWSError(error_msg)
-
-	def make_request(self, extra_data, method="GET", **kwargs):
-		"""Make request to Amazon MWS API with these parameters
-		"""
-
-		# Remove all keys with an empty value because
-		# Amazon's MWS does not allow such a thing.
-		extra_data = remove_empty(extra_data)
-
-		params = {
-			'AWSAccessKeyId': self.access_key,
-			self.ACCOUNT_TYPE: self.account_id,
-			'SignatureVersion': '2',
-			'Timestamp': self.get_timestamp(),
-			'Version': self.version,
-			'SignatureMethod': 'HmacSHA256',
-		}
-		params.update(extra_data)
-		request_description = '&'.join(['%s=%s' % (k, quote(params[k], safe='-_.~')) for k in sorted(params)])
-		signature = self.calc_signature(method, request_description)
-		url = '%s%s?%s&Signature=%s' % (self.domain, self.uri, request_description, quote(signature))
-		headers = {'User-Agent': 'python-amazon-mws/0.0.1 (Language=Python)'}
-		headers.update(kwargs.get('extra_headers', {}))
-
-		try:
-			# Some might wonder as to why i don't pass the params dict as the params argument to request.
-			# My answer is, here i have to get the url parsed string of params in order to sign it, so
-			# if i pass the params dict as params to request, request will repeat that step because it will need
-			# to convert the dict to a url parsed string, so why do it twice if i can just pass the full url :).
-			response = request(method, url, data=kwargs.get('body', ''), headers=headers)
-			response.raise_for_status()
-			# When retrieving data from the response object,
-			# be aware that response.content returns the content in bytes while response.text calls
-			# response.content and converts it to unicode.
-			data = response.content
-
-			# I do not check the headers to decide which content structure to server simply because sometimes
-			# Amazon's MWS API returns XML error responses with "text/plain" as the Content-Type.
-			try:
-				parsed_response = DictWrapper(data, extra_data.get("Action") + "Result")
-			except XMLError:
-				parsed_response = DataWrapper(data, response.headers)
-
-		except HTTPError as e:
-			error = MWSError(str(e))
-			error.response = e.response
-			raise error
-
-		# Store the response object in the parsed_response for quick access
-		parsed_response.response = response
-		return parsed_response
-
-	def get_service_status(self):
-		"""
-			Returns a GREEN, GREEN_I, YELLOW or RED status.
-			Depending on the status/availability of the API its being called from.
-		"""
-
-		return self.make_request(extra_data=dict(Action='GetServiceStatus'))
-
-	def calc_signature(self, method, request_description):
-		"""Calculate MWS signature to interface with Amazon
-		"""
-		sig_data = method + '\n' + self.domain.replace('https://', '').lower() + '\n' + self.uri + '\n' + request_description
-		sig_data = sig_data.encode('utf-8')
-		secret_key = self.secret_key.encode('utf-8')
-		digest = hmac.new(secret_key, sig_data, hashlib.sha256).digest()
-		return base64.b64encode(digest).decode('utf-8')
-
-	def get_timestamp(self):
-		"""
-			Returns the current timestamp in proper format.
-		"""
-		return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime())
-
-	def enumerate_param(self, param, values):
-		"""
-			Builds a dictionary of an enumerated parameter.
-			Takes any iterable and returns a dictionary.
-			ie.
-			enumerate_param('MarketplaceIdList.Id', (123, 345, 4343))
-			returns
-			{
-				MarketplaceIdList.Id.1: 123,
-				MarketplaceIdList.Id.2: 345,
-				MarketplaceIdList.Id.3: 4343
-			}
-		"""
-		params = {}
-		if values is not None:
-			if not param.endswith('.'):
-				param = "%s." % param
-			for num, value in enumerate(values):
-				params['%s%d' % (param, (num + 1))] = value
-		return params
-
-
-class Feeds(MWS):
-	""" Amazon MWS Feeds API """
-
-	ACCOUNT_TYPE = "Merchant"
-
-	def submit_feed(self, feed, feed_type, marketplaceids=None,
-					content_type="text/xml", purge='false'):
-		"""
-		Uploads a feed ( xml or .tsv ) to the seller's inventory.
-		Can be used for creating/updating products on Amazon.
-		"""
-		data = dict(Action='SubmitFeed',
-					FeedType=feed_type,
-					PurgeAndReplace=purge)
-		data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids))
-		md = calc_md5(feed)
-		return self.make_request(data, method="POST", body=feed,
-								extra_headers={'Content-MD5': md, 'Content-Type': content_type})
-
-	def get_feed_submission_list(self, feedids=None, max_count=None, feedtypes=None,
-								processingstatuses=None, fromdate=None, todate=None):
-		"""
-		Returns a list of all feed submissions submitted in the previous 90 days.
-		That match the query parameters.
-		"""
-
-		data = dict(Action='GetFeedSubmissionList',
-					MaxCount=max_count,
-					SubmittedFromDate=fromdate,
-					SubmittedToDate=todate,)
-		data.update(self.enumerate_param('FeedSubmissionIdList.Id', feedids))
-		data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
-		data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses))
-		return self.make_request(data)
-
-	def get_submission_list_by_next_token(self, token):
-		data = dict(Action='GetFeedSubmissionListByNextToken', NextToken=token)
-		return self.make_request(data)
-
-	def get_feed_submission_count(self, feedtypes=None, processingstatuses=None, fromdate=None, todate=None):
-		data = dict(Action='GetFeedSubmissionCount',
-					SubmittedFromDate=fromdate,
-					SubmittedToDate=todate)
-		data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
-		data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses))
-		return self.make_request(data)
-
-	def cancel_feed_submissions(self, feedids=None, feedtypes=None, fromdate=None, todate=None):
-		data = dict(Action='CancelFeedSubmissions',
-					SubmittedFromDate=fromdate,
-					SubmittedToDate=todate)
-		data.update(self.enumerate_param('FeedSubmissionIdList.Id.', feedids))
-		data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
-		return self.make_request(data)
-
-	def get_feed_submission_result(self, feedid):
-		data = dict(Action='GetFeedSubmissionResult', FeedSubmissionId=feedid)
-		return self.make_request(data)
-
-class Reports(MWS):
-	""" Amazon MWS Reports API """
-
-	ACCOUNT_TYPE = "Merchant"
-
-	## REPORTS ###
-
-	def get_report(self, report_id):
-		data = dict(Action='GetReport', ReportId=report_id)
-		return self.make_request(data)
-
-	def get_report_count(self, report_types=(), acknowledged=None, fromdate=None, todate=None):
-		data = dict(Action='GetReportCount',
-					Acknowledged=acknowledged,
-					AvailableFromDate=fromdate,
-					AvailableToDate=todate)
-		data.update(self.enumerate_param('ReportTypeList.Type.', report_types))
-		return self.make_request(data)
-
-	def get_report_list(self, requestids=(), max_count=None, types=(), acknowledged=None,
-						fromdate=None, todate=None):
-		data = dict(Action='GetReportList',
-					Acknowledged=acknowledged,
-					AvailableFromDate=fromdate,
-					AvailableToDate=todate,
-					MaxCount=max_count)
-		data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids))
-		data.update(self.enumerate_param('ReportTypeList.Type.', types))
-		return self.make_request(data)
-
-	def get_report_list_by_next_token(self, token):
-		data = dict(Action='GetReportListByNextToken', NextToken=token)
-		return self.make_request(data)
-
-	def get_report_request_count(self, report_types=(), processingstatuses=(), fromdate=None, todate=None):
-		data = dict(Action='GetReportRequestCount',
-					RequestedFromDate=fromdate,
-					RequestedToDate=todate)
-		data.update(self.enumerate_param('ReportTypeList.Type.', report_types))
-		data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses))
-		return self.make_request(data)
-
-	def get_report_request_list(self, requestids=(), types=(), processingstatuses=(),
-								max_count=None, fromdate=None, todate=None):
-		data = dict(Action='GetReportRequestList',
-					MaxCount=max_count,
-					RequestedFromDate=fromdate,
-					RequestedToDate=todate)
-		data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids))
-		data.update(self.enumerate_param('ReportTypeList.Type.', types))
-		data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses))
-		return self.make_request(data)
-
-	def get_report_request_list_by_next_token(self, token):
-		data = dict(Action='GetReportRequestListByNextToken', NextToken=token)
-		return self.make_request(data)
-
-	def request_report(self, report_type, start_date=None, end_date=None, marketplaceids=()):
-		data = dict(Action='RequestReport',
-					ReportType=report_type,
-					StartDate=start_date,
-					EndDate=end_date)
-		data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids))
-		return self.make_request(data)
-
-	### ReportSchedule ###
-
-	def get_report_schedule_list(self, types=()):
-		data = dict(Action='GetReportScheduleList')
-		data.update(self.enumerate_param('ReportTypeList.Type.', types))
-		return self.make_request(data)
-
-	def get_report_schedule_count(self, types=()):
-		data = dict(Action='GetReportScheduleCount')
-		data.update(self.enumerate_param('ReportTypeList.Type.', types))
-		return self.make_request(data)
-
-
-class Orders(MWS):
-	""" Amazon Orders API """
-
-	URI = "/Orders/2013-09-01"
-	VERSION = "2013-09-01"
-	NS = '{https://mws.amazonservices.com/Orders/2011-01-01}'
-
-	def list_orders(self, marketplaceids, created_after=None, created_before=None, lastupdatedafter=None,
-					lastupdatedbefore=None, orderstatus=(), fulfillment_channels=(),
-					payment_methods=(), buyer_email=None, seller_orderid=None, max_results='100'):
-
-		data = dict(Action='ListOrders',
-					CreatedAfter=created_after,
-					CreatedBefore=created_before,
-					LastUpdatedAfter=lastupdatedafter,
-					LastUpdatedBefore=lastupdatedbefore,
-					BuyerEmail=buyer_email,
-					SellerOrderId=seller_orderid,
-					MaxResultsPerPage=max_results,
-					)
-		data.update(self.enumerate_param('OrderStatus.Status.', orderstatus))
-		data.update(self.enumerate_param('MarketplaceId.Id.', marketplaceids))
-		data.update(self.enumerate_param('FulfillmentChannel.Channel.', fulfillment_channels))
-		data.update(self.enumerate_param('PaymentMethod.Method.', payment_methods))
-		return self.make_request(data)
-
-	def list_orders_by_next_token(self, token):
-		data = dict(Action='ListOrdersByNextToken', NextToken=token)
-		return self.make_request(data)
-
-	def get_order(self, amazon_order_ids):
-		data = dict(Action='GetOrder')
-		data.update(self.enumerate_param('AmazonOrderId.Id.', amazon_order_ids))
-		return self.make_request(data)
-
-	def list_order_items(self, amazon_order_id):
-		data = dict(Action='ListOrderItems', AmazonOrderId=amazon_order_id)
-		return self.make_request(data)
-
-	def list_order_items_by_next_token(self, token):
-		data = dict(Action='ListOrderItemsByNextToken', NextToken=token)
-		return self.make_request(data)
-
-
-class Products(MWS):
-	""" Amazon MWS Products API """
-
-	URI = '/Products/2011-10-01'
-	VERSION = '2011-10-01'
-	NS = '{http://mws.amazonservices.com/schema/Products/2011-10-01}'
-
-	def list_matching_products(self, marketplaceid, query, contextid=None):
-		""" Returns a list of products and their attributes, ordered by
-			relevancy, based on a search query that you specify.
-			Your search query can be a phrase that describes the product
-			or it can be a product identifier such as a UPC, EAN, ISBN, or JAN.
-		"""
-		data = dict(Action='ListMatchingProducts',
-					MarketplaceId=marketplaceid,
-					Query=query,
-					QueryContextId=contextid)
-		return self.make_request(data)
-
-	def get_matching_product(self, marketplaceid, asins):
-		""" Returns a list of products and their attributes, based on a list of
-			ASIN values that you specify.
-		"""
-		data = dict(Action='GetMatchingProduct', MarketplaceId=marketplaceid)
-		data.update(self.enumerate_param('ASINList.ASIN.', asins))
-		return self.make_request(data)
-
-	def get_matching_product_for_id(self, marketplaceid, type, id):
-		""" Returns a list of products and their attributes, based on a list of
-			product identifier values (asin, sellersku, upc, ean, isbn and JAN)
-			Added in Fourth Release, API version 2011-10-01
-		"""
-		data = dict(Action='GetMatchingProductForId',
-					MarketplaceId=marketplaceid,
-					IdType=type)
-		data.update(self.enumerate_param('IdList.Id', id))
-		return self.make_request(data)
-
-	def get_competitive_pricing_for_sku(self, marketplaceid, skus):
-		""" Returns the current competitive pricing of a product,
-			based on the SellerSKU and MarketplaceId that you specify.
-		"""
-		data = dict(Action='GetCompetitivePricingForSKU', MarketplaceId=marketplaceid)
-		data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
-		return self.make_request(data)
-
-	def get_competitive_pricing_for_asin(self, marketplaceid, asins):
-		""" Returns the current competitive pricing of a product,
-			based on the ASIN and MarketplaceId that you specify.
-		"""
-		data = dict(Action='GetCompetitivePricingForASIN', MarketplaceId=marketplaceid)
-		data.update(self.enumerate_param('ASINList.ASIN.', asins))
-		return self.make_request(data)
-
-	def get_lowest_offer_listings_for_sku(self, marketplaceid, skus, condition="Any", excludeme="False"):
-		data = dict(Action='GetLowestOfferListingsForSKU',
-					MarketplaceId=marketplaceid,
-					ItemCondition=condition,
-					ExcludeMe=excludeme)
-		data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
-		return self.make_request(data)
-
-	def get_lowest_offer_listings_for_asin(self, marketplaceid, asins, condition="Any", excludeme="False"):
-		data = dict(Action='GetLowestOfferListingsForASIN',
-					MarketplaceId=marketplaceid,
-					ItemCondition=condition,
-					ExcludeMe=excludeme)
-		data.update(self.enumerate_param('ASINList.ASIN.', asins))
-		return self.make_request(data)
-
-	def get_product_categories_for_sku(self, marketplaceid, sku):
-		data = dict(Action='GetProductCategoriesForSKU',
-					MarketplaceId=marketplaceid,
-					SellerSKU=sku)
-		return self.make_request(data)
-
-	def get_product_categories_for_asin(self, marketplaceid, asin):
-		data = dict(Action='GetProductCategoriesForASIN',
-					MarketplaceId=marketplaceid,
-					ASIN=asin)
-		return self.make_request(data)
-
-	def get_my_price_for_sku(self, marketplaceid, skus, condition=None):
-		data = dict(Action='GetMyPriceForSKU',
-					MarketplaceId=marketplaceid,
-					ItemCondition=condition)
-		data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
-		return self.make_request(data)
-
-	def get_my_price_for_asin(self, marketplaceid, asins, condition=None):
-		data = dict(Action='GetMyPriceForASIN',
-					MarketplaceId=marketplaceid,
-					ItemCondition=condition)
-		data.update(self.enumerate_param('ASINList.ASIN.', asins))
-		return self.make_request(data)
-
-
-class Sellers(MWS):
-	""" Amazon MWS Sellers API """
-
-	URI = '/Sellers/2011-07-01'
-	VERSION = '2011-07-01'
-	NS = '{http://mws.amazonservices.com/schema/Sellers/2011-07-01}'
-
-	def list_marketplace_participations(self):
-		"""
-			Returns a list of marketplaces a seller can participate in and
-			a list of participations that include seller-specific information in that marketplace.
-			The operation returns only those marketplaces where the seller's account is in an active state.
-		"""
-
-		data = dict(Action='ListMarketplaceParticipations')
-		return self.make_request(data)
-
-	def list_marketplace_participations_by_next_token(self, token):
-		"""
-			Takes a "NextToken" and returns the same information as "list_marketplace_participations".
-			Based on the "NextToken".
-		"""
-		data = dict(Action='ListMarketplaceParticipations', NextToken=token)
-		return self.make_request(data)
-
-#### Fulfillment APIs ####
-
-class InboundShipments(MWS):
-	URI = "/FulfillmentInboundShipment/2010-10-01"
-	VERSION = '2010-10-01'
-
-	# To be completed
-
-
-class Inventory(MWS):
-	""" Amazon MWS Inventory Fulfillment API """
-
-	URI = '/FulfillmentInventory/2010-10-01'
-	VERSION = '2010-10-01'
-	NS = "{http://mws.amazonaws.com/FulfillmentInventory/2010-10-01}"
-
-	def list_inventory_supply(self, skus=(), datetime=None, response_group='Basic'):
-		""" Returns information on available inventory """
-
-		data = dict(Action='ListInventorySupply',
-					QueryStartDateTime=datetime,
-					ResponseGroup=response_group,
-					)
-		data.update(self.enumerate_param('SellerSkus.member.', skus))
-		return self.make_request(data, "POST")
-
-	def list_inventory_supply_by_next_token(self, token):
-		data = dict(Action='ListInventorySupplyByNextToken', NextToken=token)
-		return self.make_request(data, "POST")
-
-
-class OutboundShipments(MWS):
-	URI = "/FulfillmentOutboundShipment/2010-10-01"
-	VERSION = "2010-10-01"
-	# To be completed
-
-
-class Recommendations(MWS):
-
-	""" Amazon MWS Recommendations API """
-
-	URI = '/Recommendations/2013-04-01'
-	VERSION = '2013-04-01'
-	NS = "{https://mws.amazonservices.com/Recommendations/2013-04-01}"
-
-	def get_last_updated_time_for_recommendations(self, marketplaceid):
-		"""
-		Checks whether there are active recommendations for each category for the given marketplace, and if there are,
-		returns the time when recommendations were last updated for each category.
-		"""
-
-		data = dict(Action='GetLastUpdatedTimeForRecommendations',
-					MarketplaceId=marketplaceid)
-		return self.make_request(data, "POST")
-
-	def list_recommendations(self, marketplaceid, recommendationcategory=None):
-		"""
-		Returns your active recommendations for a specific category or for all categories for a specific marketplace.
-		"""
-
-		data = dict(Action="ListRecommendations",
-					MarketplaceId=marketplaceid,
-					RecommendationCategory=recommendationcategory)
-		return self.make_request(data, "POST")
-
-	def list_recommendations_by_next_token(self, token):
-		"""
-		Returns the next page of recommendations using the NextToken parameter.
-		"""
-
-		data = dict(Action="ListRecommendationsByNextToken",
-					NextToken=token)
-		return self.make_request(data, "POST")
-
-class Finances(MWS):
-	""" Amazon Finances API"""
-	URI = '/Finances/2015-05-01'
-	VERSION = '2015-05-01'
-	NS = "{https://mws.amazonservices.com/Finances/2015-05-01}"
-
-	def list_financial_events(self , posted_after=None, posted_before=None,
-		 					amazon_order_id=None, max_results='100'):
-
-		data = dict(Action='ListFinancialEvents',
-					PostedAfter=posted_after,
-					PostedBefore=posted_before,
-					AmazonOrderId=amazon_order_id,
-					MaxResultsPerPage=max_results,
-					)
-		return self.make_request(data)
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js
deleted file mode 100644
index f5ea804..0000000
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.json b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.json
deleted file mode 100644
index 5a678e7..0000000
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.json
+++ /dev/null
@@ -1,237 +0,0 @@
-{
- "actions": [],
- "creation": "2018-07-31 05:51:41.357047",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
-  "enable_amazon",
-  "mws_credentials",
-  "seller_id",
-  "aws_access_key_id",
-  "mws_auth_token",
-  "secret_key",
-  "column_break_4",
-  "market_place_id",
-  "region",
-  "domain",
-  "section_break_13",
-  "company",
-  "warehouse",
-  "item_group",
-  "price_list",
-  "column_break_17",
-  "customer_group",
-  "territory",
-  "customer_type",
-  "market_place_account_group",
-  "section_break_12",
-  "after_date",
-  "taxes_charges",
-  "sync_products",
-  "sync_orders",
-  "column_break_10",
-  "enable_sync",
-  "max_retry_limit"
- ],
- "fields": [
-  {
-   "default": "0",
-   "fieldname": "enable_amazon",
-   "fieldtype": "Check",
-   "label": "Enable Amazon"
-  },
-  {
-   "fieldname": "mws_credentials",
-   "fieldtype": "Section Break",
-   "label": "MWS Credentials"
-  },
-  {
-   "fieldname": "seller_id",
-   "fieldtype": "Data",
-   "in_list_view": 1,
-   "label": "Seller ID",
-   "reqd": 1
-  },
-  {
-   "fieldname": "aws_access_key_id",
-   "fieldtype": "Data",
-   "in_list_view": 1,
-   "label": "AWS Access Key ID",
-   "reqd": 1
-  },
-  {
-   "fieldname": "mws_auth_token",
-   "fieldtype": "Data",
-   "in_list_view": 1,
-   "label": "MWS Auth Token",
-   "reqd": 1
-  },
-  {
-   "fieldname": "secret_key",
-   "fieldtype": "Data",
-   "in_list_view": 1,
-   "label": "Secret Key",
-   "reqd": 1
-  },
-  {
-   "fieldname": "column_break_4",
-   "fieldtype": "Column Break"
-  },
-  {
-   "fieldname": "market_place_id",
-   "fieldtype": "Data",
-   "label": "Market Place ID",
-   "reqd": 1
-  },
-  {
-   "fieldname": "region",
-   "fieldtype": "Select",
-   "label": "Region",
-   "options": "\nAE\nAU\nBR\nCA\nCN\nDE\nES\nFR\nIN\nJP\nIT\nMX\nUK\nUS",
-   "reqd": 1
-  },
-  {
-   "fieldname": "domain",
-   "fieldtype": "Data",
-   "label": "Domain",
-   "reqd": 1
-  },
-  {
-   "fieldname": "section_break_13",
-   "fieldtype": "Section Break"
-  },
-  {
-   "fieldname": "company",
-   "fieldtype": "Link",
-   "label": "Company",
-   "options": "Company",
-   "reqd": 1
-  },
-  {
-   "fieldname": "warehouse",
-   "fieldtype": "Link",
-   "label": "Warehouse",
-   "options": "Warehouse",
-   "reqd": 1
-  },
-  {
-   "fieldname": "item_group",
-   "fieldtype": "Link",
-   "label": "Item Group",
-   "options": "Item Group",
-   "reqd": 1
-  },
-  {
-   "fieldname": "price_list",
-   "fieldtype": "Link",
-   "label": "Price List",
-   "options": "Price List",
-   "reqd": 1
-  },
-  {
-   "fieldname": "column_break_17",
-   "fieldtype": "Column Break"
-  },
-  {
-   "fieldname": "customer_group",
-   "fieldtype": "Link",
-   "label": "Customer Group",
-   "options": "Customer Group",
-   "reqd": 1
-  },
-  {
-   "fieldname": "territory",
-   "fieldtype": "Link",
-   "label": "Territory",
-   "options": "Territory",
-   "reqd": 1
-  },
-  {
-   "fieldname": "customer_type",
-   "fieldtype": "Select",
-   "label": "Customer Type",
-   "options": "Individual\nCompany",
-   "reqd": 1
-  },
-  {
-   "fieldname": "market_place_account_group",
-   "fieldtype": "Link",
-   "label": "Market Place Account Group",
-   "options": "Account",
-   "reqd": 1
-  },
-  {
-   "fieldname": "section_break_12",
-   "fieldtype": "Section Break"
-  },
-  {
-   "description": "Amazon will synch data updated after this date",
-   "fieldname": "after_date",
-   "fieldtype": "Datetime",
-   "label": "After Date",
-   "reqd": 1
-  },
-  {
-   "default": "0",
-   "description": "Get financial breakup of Taxes and charges data by Amazon ",
-   "fieldname": "taxes_charges",
-   "fieldtype": "Check",
-   "label": "Sync Taxes and Charges"
-  },
-  {
-   "fieldname": "column_break_10",
-   "fieldtype": "Column Break"
-  },
-  {
-   "default": "3",
-   "fieldname": "max_retry_limit",
-   "fieldtype": "Int",
-   "label": "Max Retry Limit"
-  },
-  {
-   "description": "Always sync your products from Amazon MWS before synching the Orders details",
-   "fieldname": "sync_products",
-   "fieldtype": "Button",
-   "label": "Sync Products",
-   "options": "get_products_details"
-  },
-  {
-   "description": "Click this button to pull your Sales Order data from Amazon MWS.",
-   "fieldname": "sync_orders",
-   "fieldtype": "Button",
-   "label": "Sync Orders",
-   "options": "get_order_details"
-  },
-  {
-   "default": "0",
-   "description": "Check this to enable a scheduled Daily synchronization routine via scheduler",
-   "fieldname": "enable_sync",
-   "fieldtype": "Check",
-   "label": "Enable Scheduled Sync"
-  }
- ],
- "issingle": 1,
- "links": [],
- "modified": "2020-04-07 14:26:20.174848",
- "modified_by": "Administrator",
- "module": "ERPNext Integrations",
- "name": "Amazon MWS Settings",
- "owner": "Administrator",
- "permissions": [
-  {
-   "create": 1,
-   "delete": 1,
-   "email": 1,
-   "print": 1,
-   "read": 1,
-   "role": "System Manager",
-   "share": 1,
-   "write": 1
-  }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py
deleted file mode 100644
index c1f460f..0000000
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-
-import dateutil
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-from frappe.model.document import Document
-
-from erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods import get_orders
-
-
-class AmazonMWSSettings(Document):
-	def validate(self):
-		if self.enable_amazon == 1:
-			self.enable_sync = 1
-			setup_custom_fields()
-		else:
-			self.enable_sync = 0
-
-	@frappe.whitelist()
-	def get_products_details(self):
-		if self.enable_amazon == 1:
-			frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details')
-
-	@frappe.whitelist()
-	def get_order_details(self):
-		if self.enable_amazon == 1:
-			after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d")
-			frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_orders', after_date=after_date)
-
-def schedule_get_order_details():
-	mws_settings = frappe.get_doc("Amazon MWS Settings")
-	if mws_settings.enable_sync and mws_settings.enable_amazon:
-		after_date = dateutil.parser.parse(mws_settings.after_date).strftime("%Y-%m-%d")
-		get_orders(after_date = after_date)
-
-def setup_custom_fields():
-	custom_fields = {
-		"Item": [dict(fieldname='amazon_item_code', label='Amazon Item Code',
-			fieldtype='Data', insert_after='series', read_only=1, print_hide=1)],
-		"Sales Order": [dict(fieldname='amazon_order_id', label='Amazon Order ID',
-			fieldtype='Data', insert_after='title', read_only=1, print_hide=1)]
-	}
-
-	create_custom_fields(custom_fields)
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.py
deleted file mode 100644
index 4be7960..0000000
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestAmazonMWSSettings(unittest.TestCase):
-	pass
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py
deleted file mode 100644
index d9dfc6f..0000000
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py
+++ /dev/null
@@ -1,104 +0,0 @@
-"""
-Created on Tue Jun 26 15:42:07 2012
-
-Borrowed from https://github.com/timotheus/ebaysdk-python
-
-@author: pierre
-"""
-
-import re
-import xml.etree.ElementTree as ET
-
-
-class object_dict(dict):
-	"""object view of dict, you can
-	>>> a = object_dict()
-	>>> a.fish = 'fish'
-	>>> a['fish']
-	'fish'
-	>>> a['water'] = 'water'
-	>>> a.water
-	'water'
- 	>>> a.test = {'value': 1}
-	>>> a.test2 = object_dict({'name': 'test2', 'value': 2})
-	>>> a.test, a.test2.name, a.test2.value
-	(1, 'test2', 2)
-	"""
-	def __init__(self, initd=None):
-		if initd is None:
-			initd = {}
-		dict.__init__(self, initd)
-
-	def __getattr__(self, item):
-
-		try:
-			d = self.__getitem__(item)
-		except KeyError:
-			return None
-
-		if isinstance(d, dict) and 'value' in d and len(d) == 1:
-			return d['value']
-		else:
-			return d
-
-	# if value is the only key in object, you can omit it
-	def __setstate__(self, item):
-		return False
-
-	def __setattr__(self, item, value):
-		self.__setitem__(item, value)
-
-	def getvalue(self, item, value=None):
-		return self.get(item, {}).get('value', value)
-
-
-class xml2dict(object):
-
-	def __init__(self):
-		pass
-
-	def _parse_node(self, node):
-		node_tree = object_dict()
-		# Save attrs and text, hope there will not be a child with same name
-		if node.text:
-			node_tree.value = node.text
-		for (k, v) in node.attrib.items():
-			k, v = self._namespace_split(k, object_dict({'value':v}))
-			node_tree[k] = v
-		#Save childrens
-		for child in node.getchildren():
-			tag, tree = self._namespace_split(child.tag,
-											self._parse_node(child))
-			if tag not in node_tree:  # the first time, so store it in dict
-				node_tree[tag] = tree
-				continue
-			old = node_tree[tag]
-			if not isinstance(old, list):
-				node_tree.pop(tag)
-				node_tree[tag] = [old]  # multi times, so change old dict to a list
-			node_tree[tag].append(tree)  # add the new one
-
-		return node_tree
-
-	def _namespace_split(self, tag, value):
-		"""
-		Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
-		ns = http://cs.sfsu.edu/csc867/myscheduler
-		name = patients
-		"""
-		result = re.compile(r"\{(.*)\}(.*)").search(tag)
-		if result:
-			value.namespace, tag = result.groups()
-
-		return (tag, value)
-
-	def parse(self, file):
-		"""parse a xml file to a dict"""
-		f = open(file, 'r')
-		return self.fromstring(f.read())
-
-	def fromstring(self, s):
-		"""parse a string"""
-		t = ET.fromstring(s)
-		root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
-		return object_dict({root_tag: root_tree})
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
index a8119ac..f02f76e 100644
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
+++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
@@ -13,7 +13,7 @@
 
 
 class GoCardlessSettings(Document):
-	supported_currencies = ["EUR", "DKK", "GBP", "SEK"]
+	supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"]
 
 	def validate(self):
 		self.initialize_client()
@@ -80,7 +80,7 @@
 
 	def validate_transaction_currency(self, currency):
 		if currency not in self.supported_currencies:
-			frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency))
+			frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency))
 
 	def get_payment_url(self, **kwargs):
 		return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs)))
diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
index 45077aa..1f2619b 100644
--- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
@@ -30,17 +30,6 @@
    "type": "Link"
   },
   {
-   "dependencies": "",
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Amazon MWS Settings",
-   "link_count": 0,
-   "link_to": "Amazon MWS Settings",
-   "link_type": "DocType",
-   "onboard": 0,
-   "type": "Link"
-  },
-  {
    "hidden": 0,
    "is_query_report": 0,
    "label": "Payments",
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index c742487..f8c4288 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -331,7 +331,6 @@
 	"hourly": [
 		'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
 		"erpnext.accounts.doctype.subscription.subscription.process_all",
-		"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
 		"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
 		"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
 		"erpnext.projects.doctype.project.project.hourly_reminder",
@@ -339,7 +338,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/hr/doctype/employee_group_table/employee_group_table.json b/erpnext/hr/doctype/employee_group_table/employee_group_table.json
index 4e0045c..54eb8c6 100644
--- a/erpnext/hr/doctype/employee_group_table/employee_group_table.json
+++ b/erpnext/hr/doctype/employee_group_table/employee_group_table.json
@@ -27,12 +27,13 @@
    "fetch_from": "employee.user_id",
    "fieldname": "user_id",
    "fieldtype": "Data",
+   "in_list_view": 1,
    "label": "ERPNext User ID",
    "read_only": 1
   }
  ],
  "istable": 1,
- "modified": "2019-06-06 10:41:20.313756",
+ "modified": "2022-02-13 19:44:21.302938",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Employee Group Table",
@@ -42,4 +43,4 @@
  "sort_field": "modified",
  "sort_order": "DESC",
  "track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 6b85927..6d27f4a 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -546,7 +546,7 @@
 		from erpnext.hr.utils import allocate_earned_leaves
 		i = 0
 		while(i<14):
-			allocate_earned_leaves()
+			allocate_earned_leaves(ignore_duplicates=True)
 			i += 1
 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
 
@@ -554,7 +554,7 @@
 		frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0)
 		i = 0
 		while(i<6):
-			allocate_earned_leaves()
+			allocate_earned_leaves(ignore_duplicates=True)
 			i += 1
 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
 
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
index 355370f..c11a821 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -8,11 +8,10 @@
 import frappe
 from frappe import _, bold
 from frappe.model.document import Document
-from frappe.utils import date_diff, flt, formatdate, get_datetime, getdate
+from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate
 
 
 class LeavePolicyAssignment(Document):
-
 	def validate(self):
 		self.validate_policy_assignment_overlap()
 		self.set_dates()
@@ -94,10 +93,12 @@
 			new_leaves_allocated = 0
 
 		elif leave_type_details.get(leave_type).is_earned_leave == 1:
-			if self.assignment_based_on == "Leave Period":
-				new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining)
-			else:
+			if not self.assignment_based_on:
 				new_leaves_allocated = 0
+			else:
+				# get leaves for past months if assignment is based on Leave Period / Joining Date
+				new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining)
+
 		# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
 		elif getdate(date_of_joining) > getdate(self.effective_from):
 			remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1))
@@ -108,21 +109,24 @@
 	def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
 		from erpnext.hr.utils import get_monthly_earned_leave
 
-		current_month = get_datetime().month
-		current_year = get_datetime().year
+		current_date = frappe.flags.current_date or getdate()
+		if current_date > getdate(self.effective_to):
+			current_date = getdate(self.effective_to)
 
-		from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date")
-		if getdate(date_of_joining) > getdate(from_date):
-			from_date = date_of_joining
-
-		from_date_month = get_datetime(from_date).month
-		from_date_year = get_datetime(from_date).year
+		from_date = getdate(self.effective_from)
+		if getdate(date_of_joining) > from_date:
+			from_date = getdate(date_of_joining)
 
 		months_passed = 0
-		if current_year == from_date_year and current_month > from_date_month:
-			months_passed = current_month - from_date_month
-		elif current_year > from_date_year:
-			months_passed = (12 - from_date_month) + current_month
+		based_on_doj = leave_type_details.get(leave_type).based_on_date_of_joining
+
+		if current_date.year == from_date.year and current_date.month >= from_date.month:
+			months_passed = current_date.month - from_date.month
+			months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj)
+
+		elif current_date.year > from_date.year:
+			months_passed = (12 - from_date.month) + current_date.month
+			months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj)
 
 		if months_passed > 0:
 			monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
@@ -134,6 +138,23 @@
 		return new_leaves_allocated
 
 
+def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj):
+	date = getdate(frappe.flags.current_date) or getdate()
+
+	if based_on_doj:
+		# if leave type allocation is based on DOJ, and the date of assignment creation is same as DOJ,
+		# then the month should be considered
+		if date.day == date_of_joining.day:
+			months_passed += 1
+	else:
+		last_day_of_month = get_last_day(date)
+		# if its the last day of the month, then that month should be considered
+		if last_day_of_month == date:
+			months_passed += 1
+
+	return months_passed
+
+
 @frappe.whitelist()
 def create_assignment_for_multiple_employees(employees, data):
 
@@ -168,7 +189,7 @@
 def get_leave_type_details():
 	leave_type_details = frappe._dict()
 	leave_types = frappe.get_all("Leave Type",
-		fields=["name", "is_lwp", "is_earned_leave", "is_compensatory",
+		fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "based_on_date_of_joining",
 			"is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"])
 	for d in leave_types:
 		leave_type_details.setdefault(d.name, d)
diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
index 3b7f8ec..a19ddce 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
@@ -4,7 +4,7 @@
 import unittest
 
 import frappe
-from frappe.utils import add_months, get_first_day, getdate
+from frappe.utils import add_months, get_first_day, get_last_day, getdate
 
 from erpnext.hr.doctype.leave_application.test_leave_application import (
 	get_employee,
@@ -20,36 +20,31 @@
 class TestLeavePolicyAssignment(unittest.TestCase):
 	def setUp(self):
 		for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
-			frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
+			frappe.db.delete(doctype)
+
+		employee = get_employee()
+		self.original_doj = employee.date_of_joining
+		self.employee = employee
 
 	def test_grant_leaves(self):
 		leave_period = get_leave_period()
-		employee = get_employee()
-
-		# create the leave policy with leave type "_Test Leave Type", allocation = 10
+		# allocation = 10
 		leave_policy = create_leave_policy()
 		leave_policy.submit()
 
-
 		data = {
 			"assignment_based_on": "Leave Period",
 			"leave_policy": leave_policy.name,
 			"leave_period": leave_period.name
 		}
-
-		leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
-
-		leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
-		leave_policy_assignment_doc.reload()
-
-		self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
+		leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+		self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1)
 
 		leave_allocation = frappe.get_list("Leave Allocation", filters={
-			"employee": employee.name,
+			"employee": self.employee.name,
 			"leave_policy":leave_policy.name,
 			"leave_policy_assignment": leave_policy_assignments[0],
 			"docstatus": 1})[0]
-
 		leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
 
 		self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
@@ -61,63 +56,46 @@
 
 	def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self):
 		leave_period = get_leave_period()
-		employee = get_employee()
-
 		# create the leave policy with leave type "_Test Leave Type", allocation = 10
 		leave_policy = create_leave_policy()
 		leave_policy.submit()
 
-
 		data = {
 			"assignment_based_on": "Leave Period",
 			"leave_policy": leave_policy.name,
 			"leave_period": leave_period.name
 		}
-
-		leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
-
-		leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
-		leave_policy_assignment_doc.reload()
-
+		leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
 
 		# every leave is allocated no more leave can be granted now
-		self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
-
+		self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1)
 		leave_allocation = frappe.get_list("Leave Allocation", filters={
-			"employee": employee.name,
+			"employee": self.employee.name,
 			"leave_policy":leave_policy.name,
 			"leave_policy_assignment": leave_policy_assignments[0],
 			"docstatus": 1})[0]
 
 		leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
-
-		# User all allowed to grant leave when there is no allocation against assignment
 		leave_alloc_doc.cancel()
 		leave_alloc_doc.delete()
-
-		leave_policy_assignment_doc.reload()
-
-
-		# User are now allowed to grant leave
-		self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
+		self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 0)
 
 	def test_earned_leave_allocation(self):
 		leave_period = create_leave_period("Test Earned Leave Period")
-		employee = get_employee()
 		leave_type = create_earned_leave_type("Test Earned Leave")
 
 		leave_policy = frappe.get_doc({
 			"doctype": "Leave Policy",
 			"title": "Test Leave Policy",
 			"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}]
-		}).insert()
+		}).submit()
 
 		data = {
 			"assignment_based_on": "Leave Period",
 			"leave_policy": leave_policy.name,
 			"leave_period": leave_period.name
 		}
-		leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
+		leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
 
 		# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
 		leaves_allocated = frappe.db.get_value("Leave Allocation", {
@@ -125,11 +103,200 @@
 		}, "total_leaves_allocated")
 		self.assertEqual(leaves_allocated, 0)
 
+	def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self):
+		leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -1)))
+
+		# Case 1: assignment created one month after the leave period, should allocate 1 leave
+		frappe.flags.current_date = get_first_day(getdate())
+		data = {
+			"assignment_based_on": "Leave Period",
+			"leave_policy": leave_policy.name,
+			"leave_period": leave_period.name
+		}
+		leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {
+			"leave_policy_assignment": leave_policy_assignments[0]
+		}, "total_leaves_allocated")
+		self.assertEqual(leaves_allocated, 1)
+
+	def test_earned_leave_alloc_for_passed_months_on_month_end_based_on_leave_period(self):
+		leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)))
+		# Case 2: assignment created on the last day of the leave period's latter month
+		# should allocate 1 leave for current month even though the month has not ended
+		# since the daily job might have already executed
+		frappe.flags.current_date = get_last_day(getdate())
+
+		data = {
+			"assignment_based_on": "Leave Period",
+			"leave_policy": leave_policy.name,
+			"leave_period": leave_period.name
+		}
+		leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {
+			"leave_policy_assignment": leave_policy_assignments[0]
+		}, "total_leaves_allocated")
+		self.assertEqual(leaves_allocated, 3)
+
+		# if the daily job is not completed yet, there is another check present
+		# to ensure leave is not already allocated to avoid duplication
+		from erpnext.hr.utils import allocate_earned_leaves
+		allocate_earned_leaves()
+
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {
+			"leave_policy_assignment": leave_policy_assignments[0]
+		}, "total_leaves_allocated")
+		self.assertEqual(leaves_allocated, 3)
+
+	def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self):
+		from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
+
+		leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)))
+		# initial leave allocation = 5
+		leave_allocation = create_leave_allocation(employee=self.employee.name, employee_name=self.employee.employee_name, leave_type="Test Earned Leave",
+			from_date=add_months(getdate(), -12), to_date=add_months(getdate(), -3), new_leaves_allocated=5, carry_forward=0)
+		leave_allocation.submit()
+
+		# Case 3: assignment created on the last day of the leave period's latter month with carry forwarding
+		frappe.flags.current_date = get_last_day(add_months(getdate(), -1))
+		data = {
+			"assignment_based_on": "Leave Period",
+			"leave_policy": leave_policy.name,
+			"leave_period": leave_period.name,
+			"carry_forward": 1
+		}
+		# carry forwarded leaves = 5, 3 leaves allocated for passed months
+		leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+
+		details = frappe.db.get_value("Leave Allocation", {
+			"leave_policy_assignment": leave_policy_assignments[0]
+		}, ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], as_dict=True)
+		self.assertEqual(details.new_leaves_allocated, 2)
+		self.assertEqual(details.unused_leaves, 5)
+		self.assertEqual(details.total_leaves_allocated, 7)
+
+		# if the daily job is not completed yet, there is another check present
+		# to ensure leave is not already allocated to avoid duplication
+		from erpnext.hr.utils import is_earned_leave_already_allocated
+		frappe.flags.current_date = get_last_day(getdate())
+
+		allocation = frappe.get_doc("Leave Allocation", details.name)
+		# 1 leave is still pending to be allocated, irrespective of carry forwarded leaves
+		self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation))
+
+	def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self):
+		# tests leave alloc for earned leaves for assignment based on joining date in policy assignment
+		leave_type = create_earned_leave_type("Test Earned Leave")
+		leave_policy = frappe.get_doc({
+			"doctype": "Leave Policy",
+			"title": "Test Leave Policy",
+			"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
+		}).submit()
+
+		# joining date set to 2 months back
+		self.employee.date_of_joining = get_first_day(add_months(getdate(), -2))
+		self.employee.save()
+
+		# assignment created on the last day of the current month
+		frappe.flags.current_date = get_last_day(getdate())
+		data = {
+			"assignment_based_on": "Joining Date",
+			"leave_policy": leave_policy.name
+		}
+		leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
+			"total_leaves_allocated")
+		effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from")
+		self.assertEqual(effective_from, self.employee.date_of_joining)
+		self.assertEqual(leaves_allocated, 3)
+
+		# to ensure leave is not already allocated to avoid duplication
+		from erpnext.hr.utils import allocate_earned_leaves
+		frappe.flags.current_date = get_last_day(getdate())
+		allocate_earned_leaves()
+
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
+			"total_leaves_allocated")
+		self.assertEqual(leaves_allocated, 3)
+
+	def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self):
+		# tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type
+		leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)), based_on_doj=True)
+
+		# joining date set to 2 months back
+		self.employee.date_of_joining = get_first_day(add_months(getdate(), -2))
+		self.employee.save()
+
+		# assignment created on the same day of the current month, should allocate leaves including the current month
+		frappe.flags.current_date = get_first_day(getdate())
+
+		data = {
+			"assignment_based_on": "Leave Period",
+			"leave_policy": leave_policy.name,
+			"leave_period": leave_period.name
+		}
+		leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {
+			"leave_policy_assignment": leave_policy_assignments[0]
+		}, "total_leaves_allocated")
+		self.assertEqual(leaves_allocated, 3)
+
+		# if the daily job is not completed yet, there is another check present
+		# to ensure leave is not already allocated to avoid duplication
+		from erpnext.hr.utils import allocate_earned_leaves
+		frappe.flags.current_date = get_first_day(getdate())
+		allocate_earned_leaves()
+
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {
+			"leave_policy_assignment": leave_policy_assignments[0]
+		}, "total_leaves_allocated")
+		self.assertEqual(leaves_allocated, 3)
+
+	def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self):
+		# tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type
+		leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True)
+		leave_policy = frappe.get_doc({
+			"doctype": "Leave Policy",
+			"title": "Test Leave Policy",
+			"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
+		}).submit()
+
+		# joining date set to 2 months back
+		# leave should be allocated for current month too since this day is same as the joining day
+		self.employee.date_of_joining = get_first_day(add_months(getdate(), -2))
+		self.employee.save()
+
+		# assignment created on the first day of the current month
+		frappe.flags.current_date = get_first_day(getdate())
+		data = {
+			"assignment_based_on": "Joining Date",
+			"leave_policy": leave_policy.name
+		}
+		leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
+			"total_leaves_allocated")
+		effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from")
+		self.assertEqual(effective_from, self.employee.date_of_joining)
+		self.assertEqual(leaves_allocated, 3)
+
+		# to ensure leave is not already allocated to avoid duplication
+		from erpnext.hr.utils import allocate_earned_leaves
+		frappe.flags.current_date = get_first_day(getdate())
+		allocate_earned_leaves()
+
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
+			"total_leaves_allocated")
+		self.assertEqual(leaves_allocated, 3)
+
 	def tearDown(self):
 		frappe.db.rollback()
+		frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
+		frappe.flags.current_date = None
 
 
-def create_earned_leave_type(leave_type):
+def create_earned_leave_type(leave_type, based_on_doj=False):
 	frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
 
 	return frappe.get_doc(dict(
@@ -138,13 +305,15 @@
 		is_earned_leave=1,
 		earned_leave_frequency="Monthly",
 		rounding=0.5,
-		max_leaves_allowed=6
+		is_carry_forward=1,
+		based_on_date_of_joining=based_on_doj
 	)).insert()
 
 
-def create_leave_period(name):
+def create_leave_period(name, start_date=None):
 	frappe.delete_doc_if_exists("Leave Period", name, force=1)
-	start_date = get_first_day(getdate())
+	if not start_date:
+		start_date = get_first_day(getdate())
 
 	return frappe.get_doc(dict(
 		name=name,
@@ -153,4 +322,17 @@
 		to_date=add_months(start_date, 12),
 		company="_Test Company",
 		is_active=1
-	)).insert()
\ No newline at end of file
+	)).insert()
+
+
+def setup_leave_period_and_policy(start_date, based_on_doj=False):
+	leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj)
+	leave_period = create_leave_period("Test Earned Leave Period",
+		start_date=start_date)
+	leave_policy = frappe.get_doc({
+		"doctype": "Leave Policy",
+		"title": "Test Leave Policy",
+		"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
+	}).insert()
+
+	return leave_period, leave_policy
\ No newline at end of file
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 0febce1..c174047 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -237,7 +237,7 @@
 
 		create_leave_encashment(leave_allocation=leave_allocation)
 
-def allocate_earned_leaves():
+def allocate_earned_leaves(ignore_duplicates=False):
 	'''Allocate earned leaves to Employees'''
 	e_leave_types = get_earned_leaves()
 	today = getdate()
@@ -261,13 +261,13 @@
 
 			from_date=allocation.from_date
 
-			if e_leave_type.based_on_date_of_joining_date:
+			if e_leave_type.based_on_date_of_joining:
 				from_date  = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
 
-			if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date):
-				update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
+			if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining):
+				update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates)
 
-def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
+def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False):
 		earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding)
 
 		allocation = frappe.get_doc('Leave Allocation', allocation.name)
@@ -277,9 +277,12 @@
 			new_allocation = e_leave_type.max_leaves_allowed
 
 		if new_allocation != allocation.total_leaves_allocated:
-			allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
 			today_date = today()
-			create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
+
+			if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation):
+				allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
+				create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
+
 
 def get_monthly_earned_leave(annual_leaves, frequency, rounding):
 	earned_leaves = 0.0
@@ -297,6 +300,28 @@
 	return earned_leaves
 
 
+def is_earned_leave_already_allocated(allocation, annual_allocation):
+	from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
+		get_leave_type_details,
+	)
+
+	leave_type_details = get_leave_type_details()
+	date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
+
+	assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment)
+	leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type,
+		annual_allocation, leave_type_details, date_of_joining)
+
+	# exclude carry-forwarded leaves while checking for leave allocation for passed months
+	num_allocations = allocation.total_leaves_allocated
+	if allocation.unused_leaves:
+		num_allocations -= allocation.unused_leaves
+
+	if num_allocations >= leaves_for_passed_months:
+		return True
+	return False
+
+
 def get_leave_allocations(date, leave_type):
 	return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
 		from `tabLeave Allocation`
@@ -318,7 +343,7 @@
 	allocation.unused_leaves = 0
 	allocation.create_leave_ledger_entry()
 
-def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date):
+def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining):
 	import calendar
 
 	from dateutil import relativedelta
@@ -329,7 +354,7 @@
 	#last day of month
 	last_day =  calendar.monthrange(to_date.year, to_date.month)[1]
 
-	if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day):
+	if (from_date.day == to_date.day and based_on_date_of_joining) or (not based_on_date_of_joining and to_date.day == last_day):
 		if frequency == "Monthly":
 			return True
 		elif frequency == "Quarterly" and rd.months % 3:
diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
index f9c201a..940a1bb 100644
--- a/erpnext/loan_management/doctype/loan/loan.js
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -46,7 +46,7 @@
 			});
 		});
 
-		$.each(["payment_account", "loan_account"], function (i, field) {
+		$.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
 			frm.set_query(field, function () {
 				return {
 					"filters": {
@@ -88,6 +88,10 @@
 				frm.add_custom_button(__('Loan Write Off'), function() {
 					frm.trigger("make_loan_write_off_entry");
 				},__('Create'));
+
+				frm.add_custom_button(__('Loan Refund'), function() {
+					frm.trigger("make_loan_refund");
+				},__('Create'));
 			}
 		}
 		frm.trigger("toggle_fields");
@@ -155,6 +159,21 @@
 		})
 	},
 
+	make_loan_refund: function(frm) {
+		frappe.call({
+			args: {
+				"loan": frm.doc.name
+			},
+			method: "erpnext.loan_management.doctype.loan.loan.make_refund_jv",
+			callback: function (r) {
+				if (r.message) {
+					let doc = frappe.model.sync(r.message)[0];
+					frappe.set_route("Form", doc.doctype, doc.name);
+				}
+			}
+		})
+	},
+
 	request_loan_closure: function(frm) {
 		frappe.confirm(__("Do you really want to close this loan"),
 			function() {
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index af26f7b..196f36f 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -2,7 +2,7 @@
  "actions": [],
  "allow_import": 1,
  "autoname": "ACC-LOAN-.YYYY.-.#####",
- "creation": "2019-08-29 17:29:18.176786",
+ "creation": "2022-01-25 10:30:02.294967",
  "doctype": "DocType",
  "document_type": "Document",
  "editable_grid": 1,
@@ -34,6 +34,7 @@
   "is_term_loan",
   "account_info",
   "mode_of_payment",
+  "disbursement_account",
   "payment_account",
   "column_break_9",
   "loan_account",
@@ -356,12 +357,21 @@
    "fieldtype": "Date",
    "label": "Closure Date",
    "read_only": 1
+  },
+  {
+   "fetch_from": "loan_type.disbursement_account",
+   "fieldname": "disbursement_account",
+   "fieldtype": "Link",
+   "label": "Disbursement Account",
+   "options": "Account",
+   "read_only": 1,
+   "reqd": 1
   }
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-10-12 18:10:32.360818",
+ "modified": "2022-01-25 16:29:16.325501",
  "modified_by": "Administrator",
  "module": "Loan Management",
  "name": "Loan",
@@ -391,5 +401,6 @@
  "search_fields": "posting_date",
  "sort_field": "creation",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index f660a24..b798e08 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -10,6 +10,7 @@
 from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate
 
 import erpnext
+from erpnext.accounts.doctype.journal_entry.journal_entry import get_payment_entry
 from erpnext.controllers.accounts_controller import AccountsController
 from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
 from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import (
@@ -233,17 +234,15 @@
 	loan_type = frappe.get_value('Loan', loan, 'loan_type')
 	write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
 
-	# checking greater than 0 as there may be some minor precision error
-	if not pending_amount:
-		frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
-	elif pending_amount < write_off_limit:
+	if pending_amount and abs(pending_amount) < write_off_limit:
 		# Auto create loan write off and update status as loan closure requested
 		write_off = make_loan_write_off(loan)
 		write_off.submit()
-		frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
-	else:
+	elif pending_amount > 0:
 		frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
 
+	frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
+
 @frappe.whitelist()
 def get_loan_application(loan_application):
 	loan = frappe.get_doc("Loan Application", loan_application)
@@ -400,4 +399,39 @@
 	if getdate(date) == get_last_day(date):
 		return get_last_day(add_months(date, 1))
 	else:
-		return add_months(date, 1)
\ No newline at end of file
+		return add_months(date, 1)
+
+@frappe.whitelist()
+def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, submit=0):
+	loan_details = frappe.db.get_value('Loan', loan, ['applicant_type', 'applicant',
+		'loan_account', 'payment_account', 'posting_date', 'company', 'name',
+		'total_payment', 'total_principal_paid'], as_dict=1)
+
+	loan_details.doctype = 'Loan'
+	loan_details[loan_details.applicant_type.lower()] = loan_details.applicant
+
+	if not amount:
+		amount = flt(loan_details.total_principal_paid - loan_details.total_payment)
+
+		if amount < 0:
+			frappe.throw(_('No excess amount pending for refund'))
+
+	refund_jv = get_payment_entry(loan_details, {
+		"party_type": loan_details.applicant_type,
+		"party_account": loan_details.loan_account,
+		"amount_field_party": 'debit_in_account_currency',
+		"amount_field_bank": 'credit_in_account_currency',
+		"amount": amount,
+		"bank_account": loan_details.payment_account
+	})
+
+	if reference_number:
+		refund_jv.cheque_no = reference_number
+
+	if reference_date:
+		refund_jv.cheque_date = reference_date
+
+	if submit:
+		refund_jv.submit()
+
+	return refund_jv
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index 1676c21..5ebb2e1 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -42,16 +42,17 @@
 		create_loan_type("Personal Loan", 500000, 8.4,
 			is_term_loan=1,
 			mode_of_payment='Cash',
+			disbursement_account='Disbursement Account - _TC',
 			payment_account='Payment Account - _TC',
 			loan_account='Loan Account - _TC',
 			interest_income_account='Interest Income Account - _TC',
 			penalty_income_account='Penalty Income Account - _TC')
 
-		create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
-			'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+		create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Disbursement Account - _TC',
+			'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
 
-		create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
-			'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+		create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
+			'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
 
 		create_loan_security_type()
 		create_loan_security()
@@ -679,6 +680,29 @@
 		loan.load_from_db()
 		self.assertEqual(loan.status, "Loan Closure Requested")
 
+	def test_loan_repayment_against_partially_disbursed_loan(self):
+		pledge = [{
+			"loan_security": "Test Security 1",
+			"qty": 4000.00
+		}]
+
+		loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+		create_pledge(loan_application)
+
+		loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+		loan.submit()
+
+		first_date = '2019-10-01'
+		last_date = '2019-10-30'
+
+		make_loan_disbursement_entry(loan.name, loan.loan_amount/2, disbursement_date=first_date)
+
+		loan.load_from_db()
+
+		self.assertEqual(loan.status, "Partially Disbursed")
+		create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
+			flt(loan.loan_amount/3))
+
 	def test_loan_amount_write_off(self):
 		pledge = [{
 			"loan_security": "Test Security 1",
@@ -790,6 +814,18 @@
 			"account_type": "Bank",
 		}).insert(ignore_permissions=True)
 
+	if not frappe.db.exists("Account", "Disbursement Account - _TC"):
+		frappe.get_doc({
+			"doctype": "Account",
+			"company": "_Test Company",
+			"account_name": "Disbursement Account",
+			"root_type": "Asset",
+			"report_type": "Balance Sheet",
+			"currency": "INR",
+			"parent_account": "Bank Accounts - _TC",
+			"account_type": "Bank",
+		}).insert(ignore_permissions=True)
+
 	if not frappe.db.exists("Account", "Interest Income Account - _TC"):
 		frappe.get_doc({
 			"doctype": "Account",
@@ -815,7 +851,7 @@
 		}).insert(ignore_permissions=True)
 
 def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None,
-	mode_of_payment=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
+	mode_of_payment=None, disbursement_account=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
 	repayment_method=None, repayment_periods=None):
 
 	if not frappe.db.exists("Loan Type", loan_name):
@@ -829,6 +865,7 @@
 			"penalty_interest_rate": penalty_interest_rate,
 			"grace_period_in_days": grace_period_in_days,
 			"mode_of_payment": mode_of_payment,
+			"disbursement_account": disbursement_account,
 			"payment_account": payment_account,
 			"loan_account": loan_account,
 			"interest_income_account": interest_income_account,
diff --git a/erpnext/loan_management/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py
index d367e92..640709c 100644
--- a/erpnext/loan_management/doctype/loan_application/test_loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py
@@ -15,7 +15,7 @@
 class TestLoanApplication(unittest.TestCase):
 	def setUp(self):
 		create_loan_accounts()
-		create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
+		create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Disbursement Account - _TC', 'Payment Account - _TC', 'Loan Account - _TC',
 			'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18)
 		self.applicant = make_employee("kate_loan@loan.com", "_Test Company")
 		make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR')
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index e2d758b..df3aadf 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -122,7 +122,7 @@
 		gle_map.append(
 			self.get_gl_dict({
 				"account": loan_details.loan_account,
-				"against": loan_details.payment_account,
+				"against": loan_details.disbursement_account,
 				"debit": self.disbursed_amount,
 				"debit_in_account_currency": self.disbursed_amount,
 				"against_voucher_type": "Loan",
@@ -137,7 +137,7 @@
 
 		gle_map.append(
 			self.get_gl_dict({
-				"account": loan_details.payment_account,
+				"account": loan_details.disbursement_account,
 				"against": loan_details.loan_account,
 				"credit": self.disbursed_amount,
 				"credit_in_account_currency": self.disbursed_amount,
diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
index 94ec84e..10be750 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
@@ -44,8 +44,8 @@
 	def setUp(self):
 		create_loan_accounts()
 
-		create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
-			'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+		create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
+			'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
 
 		create_loan_security_type()
 		create_loan_security()
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
index 46aaaad..e8c7750 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
@@ -30,8 +30,8 @@
 	def setUp(self):
 		create_loan_accounts()
 
-		create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
-			'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+		create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
+			'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
 
 		create_loan_security_type()
 		create_loan_security()
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 7e997e8..f3ed611 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -125,7 +125,7 @@
 
 	def update_paid_amount(self):
 		loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
-			'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
+			'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
 			'written_off_amount'], as_dict=1)
 
 		loan.update({
@@ -153,7 +153,7 @@
 
 	def mark_as_unpaid(self):
 		loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
-			'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
+			'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
 			'written_off_amount'], as_dict=1)
 
 		no_of_repayments = len(self.repayment_details)
@@ -345,7 +345,7 @@
 			gle_map.append(
 				self.get_gl_dict({
 					"account": loan_details.penalty_income_account,
-					"against": payment_account,
+					"against": loan_details.loan_account,
 					"credit": self.total_penalty_paid,
 					"credit_in_account_currency": self.total_penalty_paid,
 					"against_voucher_type": "Loan",
@@ -367,7 +367,9 @@
 				"against_voucher": self.against_loan,
 				"remarks": remarks,
 				"cost_center": self.cost_center,
-				"posting_date": getdate(self.posting_date)
+				"posting_date": getdate(self.posting_date),
+				"party_type": loan_details.applicant_type if self.repay_from_salary else '',
+				"party": loan_details.applicant if self.repay_from_salary else ''
 			})
 		)
 
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.js b/erpnext/loan_management/doctype/loan_type/loan_type.js
index 04c89c4..9f9137c 100644
--- a/erpnext/loan_management/doctype/loan_type/loan_type.js
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.js
@@ -15,7 +15,7 @@
 			});
 		});
 
-		$.each(["payment_account", "loan_account"], function (i, field) {
+		$.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
 			frm.set_query(field, function () {
 				return {
 					"filters": {
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json
index c0a5d2c..00337e4 100644
--- a/erpnext/loan_management/doctype/loan_type/loan_type.json
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.json
@@ -19,9 +19,10 @@
   "description",
   "account_details_section",
   "mode_of_payment",
+  "disbursement_account",
   "payment_account",
-  "loan_account",
   "column_break_12",
+  "loan_account",
   "interest_income_account",
   "penalty_income_account",
   "amended_from"
@@ -79,7 +80,7 @@
   {
    "fieldname": "payment_account",
    "fieldtype": "Link",
-   "label": "Payment Account",
+   "label": "Repayment Account",
    "options": "Account",
    "reqd": 1
   },
@@ -149,15 +150,23 @@
    "fieldtype": "Currency",
    "label": "Auto Write Off Amount ",
    "options": "Company:company:default_currency"
+  },
+  {
+   "fieldname": "disbursement_account",
+   "fieldtype": "Link",
+   "label": "Disbursement Account",
+   "options": "Account",
+   "reqd": 1
   }
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-04-19 18:10:57.368490",
+ "modified": "2022-01-25 16:23:57.009349",
  "modified_by": "Administrator",
  "module": "Loan Management",
  "name": "Loan Type",
+ "naming_rule": "By fieldname",
  "owner": "Administrator",
  "permissions": [
   {
@@ -181,5 +190,6 @@
   }
  ],
  "sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 4290ca3..0a468f1 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -28,9 +28,24 @@
 
 class ProductionPlan(Document):
 	def validate(self):
+		self.set_pending_qty_in_row_without_reference()
 		self.calculate_total_planned_qty()
 		self.set_status()
 
+	def set_pending_qty_in_row_without_reference(self):
+		"Set Pending Qty in independent rows (not from SO or MR)."
+		if self.docstatus > 0: # set only to initialise value before submit
+			return
+
+		for item in self.po_items:
+			if not item.get("sales_order") or not item.get("material_request"):
+				item.pending_qty = item.planned_qty
+
+	def calculate_total_planned_qty(self):
+		self.total_planned_qty = 0
+		for d in self.po_items:
+			self.total_planned_qty += flt(d.planned_qty)
+
 	def validate_data(self):
 		for d in self.get('po_items'):
 			if not d.bom_no:
@@ -263,11 +278,6 @@
 						'qty': so_detail['qty']
 				})
 
-	def calculate_total_planned_qty(self):
-		self.total_planned_qty = 0
-		for d in self.po_items:
-			self.total_planned_qty += flt(d.planned_qty)
-
 	def calculate_total_produced_qty(self):
 		self.total_produced_qty = 0
 		for d in self.po_items:
@@ -275,10 +285,11 @@
 
 		self.db_set("total_produced_qty", self.total_produced_qty, update_modified=False)
 
-	def update_produced_qty(self, produced_qty, production_plan_item):
+	def update_produced_pending_qty(self, produced_qty, production_plan_item):
 		for data in self.po_items:
 			if data.name == production_plan_item:
 				data.produced_qty = produced_qty
+				data.pending_qty = flt(data.planned_qty - produced_qty)
 				data.db_update()
 
 		self.calculate_total_produced_qty()
@@ -341,6 +352,7 @@
 
 	def get_production_items(self):
 		item_dict = {}
+
 		for d in self.po_items:
 			item_details = {
 				"production_item"		: d.item_code,
@@ -357,12 +369,12 @@
 				"production_plan"       : self.name,
 				"production_plan_item"  : d.name,
 				"product_bundle_item"	: d.product_bundle_item,
-				"planned_start_date"    : d.planned_start_date
+				"planned_start_date"    : d.planned_start_date,
+				"project"               : self.project
 			}
 
-			item_details.update({
-				"project": self.project or frappe.db.get_value("Sales Order", d.sales_order, "project")
-			})
+			if not item_details['project'] and d.sales_order:
+				item_details['project'] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
 
 			if self.get_items_from == "Material Request":
 				item_details.update({
@@ -380,39 +392,59 @@
 
 	@frappe.whitelist()
 	def make_work_order(self):
+		from erpnext.manufacturing.doctype.work_order.work_order import get_default_warehouse
+
 		wo_list, po_list = [], []
 		subcontracted_po = {}
+		default_warehouses = get_default_warehouse()
 
-		self.validate_data()
-		self.make_work_order_for_finished_goods(wo_list)
-		self.make_work_order_for_subassembly_items(wo_list, subcontracted_po)
+		self.make_work_order_for_finished_goods(wo_list, default_warehouses)
+		self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses)
 		self.make_subcontracted_purchase_order(subcontracted_po, po_list)
 		self.show_list_created_message('Work Order', wo_list)
 		self.show_list_created_message('Purchase Order', po_list)
 
-	def make_work_order_for_finished_goods(self, wo_list):
+	def make_work_order_for_finished_goods(self, wo_list, default_warehouses):
 		items_data = self.get_production_items()
 
 		for key, item in items_data.items():
 			if self.sub_assembly_items:
 				item['use_multi_level_bom'] = 0
 
+			set_default_warehouses(item, default_warehouses)
 			work_order = self.create_work_order(item)
 			if work_order:
 				wo_list.append(work_order)
 
-	def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po):
+	def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses):
 		for row in self.sub_assembly_items:
 			if row.type_of_manufacturing == 'Subcontract':
 				subcontracted_po.setdefault(row.supplier, []).append(row)
 				continue
 
-			args = {}
-			self.prepare_args_for_sub_assembly_items(row, args)
-			work_order = self.create_work_order(args)
+			work_order_data = {
+				'wip_warehouse': default_warehouses.get('wip_warehouse'),
+				'fg_warehouse': default_warehouses.get('fg_warehouse')
+			}
+
+			self.prepare_data_for_sub_assembly_items(row, work_order_data)
+			work_order = self.create_work_order(work_order_data)
 			if work_order:
 				wo_list.append(work_order)
 
+	def prepare_data_for_sub_assembly_items(self, row, wo_data):
+		for field in ["production_item", "item_name", "qty", "fg_warehouse",
+			"description", "bom_no", "stock_uom", "bom_level",
+			"production_plan_item", "schedule_date"]:
+			if row.get(field):
+				wo_data[field] = row.get(field)
+
+		wo_data.update({
+			"use_multi_level_bom": 0,
+			"production_plan": self.name,
+			"production_plan_sub_assembly_item": row.name
+		})
+
 	def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders):
 		if not subcontracted_po:
 			return
@@ -423,7 +455,7 @@
 			po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
 			po.is_subcontracted = 'Yes'
 			for row in po_list:
-				args = {
+				po_data = {
 					'item_code': row.production_item,
 					'warehouse': row.fg_warehouse,
 					'production_plan_sub_assembly_item': row.name,
@@ -433,9 +465,9 @@
 
 				for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name',
 					'description', 'production_plan_item']:
-					args[field] = row.get(field)
+					po_data[field] = row.get(field)
 
-				po.append('items', args)
+				po.append('items', po_data)
 
 			po.set_missing_values()
 			po.flags.ignore_mandatory = True
@@ -452,24 +484,9 @@
 			doc_list = [get_link_to_form(doctype, p) for p in doc_list]
 			msgprint(_("{0} created").format(comma_and(doc_list)))
 
-	def prepare_args_for_sub_assembly_items(self, row, args):
-		for field in ["production_item", "item_name", "qty", "fg_warehouse",
-			"description", "bom_no", "stock_uom", "bom_level",
-			"production_plan_item", "schedule_date"]:
-			args[field] = row.get(field)
-
-		args.update({
-			"use_multi_level_bom": 0,
-			"production_plan": self.name,
-			"production_plan_sub_assembly_item": row.name
-		})
-
 	def create_work_order(self, item):
-		from erpnext.manufacturing.doctype.work_order.work_order import (
-			OverProductionError,
-			get_default_warehouse,
-		)
-		warehouse = get_default_warehouse()
+		from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
+
 		wo = frappe.new_doc("Work Order")
 		wo.update(item)
 		wo.planned_start_date = item.get('planned_start_date') or item.get('schedule_date')
@@ -478,11 +495,11 @@
 			wo.fg_warehouse = item.get("warehouse")
 
 		wo.set_work_order_operations()
+		wo.set_required_items()
 
-		if not wo.fg_warehouse:
-			wo.fg_warehouse = warehouse.get('fg_warehouse')
 		try:
 			wo.flags.ignore_mandatory = True
+			wo.flags.ignore_validate = True
 			wo.insert()
 			return wo.name
 		except OverProductionError:
@@ -1023,3 +1040,8 @@
 
 			if d.value:
 				get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1)
+
+def set_default_warehouses(row, default_warehouses):
+	for field in ['wip_warehouse', 'fg_warehouse']:
+		if not row.get(field):
+			row[field] = default_warehouses.get(field)
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 276e708..afa1501 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -11,6 +11,7 @@
 )
 from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
 from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
 from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
 	create_stock_reconciliation,
 )
@@ -36,15 +37,21 @@
 			if not frappe.db.get_value('BOM', {'item': item}):
 				make_bom(item = item, raw_materials = raw_materials)
 
-	def test_production_plan(self):
+	def test_production_plan_mr_creation(self):
+		"Test if MRs are created for unavailable raw materials."
 		pln = create_production_plan(item_code='Test Production Item 1')
 		self.assertTrue(len(pln.mr_items), 2)
-		pln.make_material_request()
 
-		pln = frappe.get_doc('Production Plan', pln.name)
+		pln.make_material_request()
+		pln.reload()
 		self.assertTrue(pln.status, 'Material Requested')
-		material_requests = frappe.get_all('Material Request Item', fields = ['distinct parent'],
-			filters = {'production_plan': pln.name}, as_list=1)
+
+		material_requests = frappe.get_all(
+			'Material Request Item',
+			fields = ['distinct parent'],
+			filters = {'production_plan': pln.name},
+			as_list=1
+		)
 
 		self.assertTrue(len(material_requests), 2)
 
@@ -66,27 +73,42 @@
 		pln.cancel()
 
 	def test_production_plan_start_date(self):
+		"Test if Work Order has same Planned Start Date as Prod Plan."
 		planned_date = add_to_date(date=None, days=3)
-		plan = create_production_plan(item_code='Test Production Item 1', planned_start_date=planned_date)
+		plan = create_production_plan(
+			item_code='Test Production Item 1',
+			planned_start_date=planned_date
+		)
 		plan.make_work_order()
 
-		work_orders = frappe.get_all('Work Order', fields = ['name', 'planned_start_date'],
-			filters = {'production_plan': plan.name})
+		work_orders = frappe.get_all(
+			'Work Order',
+			fields = ['name', 'planned_start_date'],
+			filters = {'production_plan': plan.name}
+		)
 
 		self.assertEqual(work_orders[0].planned_start_date, planned_date)
 
 		for wo in work_orders:
 			frappe.delete_doc('Work Order', wo.name)
 
-		frappe.get_doc('Production Plan', plan.name).cancel()
+		plan.reload()
+		plan.cancel()
 
 	def test_production_plan_for_existing_ordered_qty(self):
+		"""
+		- Enable 'ignore_existing_ordered_qty'.
+		- Test if MR Planning table pulls Raw Material Qty even if it is in stock.
+		"""
 		sr1 = create_stock_reconciliation(item_code="Raw Material Item 1",
 			target="_Test Warehouse - _TC", qty=1, rate=110)
 		sr2 = create_stock_reconciliation(item_code="Raw Material Item 2",
 			target="_Test Warehouse - _TC", qty=1, rate=120)
 
-		pln = create_production_plan(item_code='Test Production Item 1', ignore_existing_ordered_qty=0)
+		pln = create_production_plan(
+			item_code='Test Production Item 1',
+			ignore_existing_ordered_qty=1
+		)
 		self.assertTrue(len(pln.mr_items), 1)
 		self.assertTrue(flt(pln.mr_items[0].quantity), 1.0)
 
@@ -95,23 +117,39 @@
 		pln.cancel()
 
 	def test_production_plan_with_non_stock_item(self):
-		pln = create_production_plan(item_code='Test Production Item 1', include_non_stock_items=0)
+		"Test if MR Planning table includes Non Stock RM."
+		pln = create_production_plan(
+			item_code='Test Production Item 1',
+			include_non_stock_items=1
+		)
 		self.assertTrue(len(pln.mr_items), 3)
 		pln.cancel()
 
 	def test_production_plan_without_multi_level(self):
-		pln = create_production_plan(item_code='Test Production Item 1', use_multi_level_bom=0)
+		"Test MR Planning table for non exploded BOM."
+		pln = create_production_plan(
+			item_code='Test Production Item 1',
+			use_multi_level_bom=0
+		)
 		self.assertTrue(len(pln.mr_items), 2)
 		pln.cancel()
 
 	def test_production_plan_without_multi_level_for_existing_ordered_qty(self):
+		"""
+		- Disable 'ignore_existing_ordered_qty'.
+		- Test if MR Planning table avoids pulling Raw Material Qty as it is in stock for
+		non exploded BOM.
+		"""
 		sr1 = create_stock_reconciliation(item_code="Raw Material Item 1",
 			target="_Test Warehouse - _TC", qty=1, rate=130)
 		sr2 = create_stock_reconciliation(item_code="Subassembly Item 1",
 			target="_Test Warehouse - _TC", qty=1, rate=140)
 
-		pln = create_production_plan(item_code='Test Production Item 1',
-			use_multi_level_bom=0, ignore_existing_ordered_qty=0)
+		pln = create_production_plan(
+			item_code='Test Production Item 1',
+			use_multi_level_bom=0,
+			ignore_existing_ordered_qty=0
+		)
 		self.assertTrue(len(pln.mr_items), 0)
 
 		sr1.cancel()
@@ -119,6 +157,7 @@
 		pln.cancel()
 
 	def test_production_plan_sales_orders(self):
+		"Test if previously fulfilled SO (with WO) is pulled into Prod Plan."
 		item = 'Test Production Item 1'
 		so = make_sales_order(item_code=item, qty=1)
 		sales_order = so.name
@@ -166,24 +205,25 @@
 		self.assertEqual(sales_orders, [])
 
 	def test_production_plan_combine_items(self):
+		"Test combining FG items in Production Plan."
 		item = 'Test Production Item 1'
-		so = make_sales_order(item_code=item, qty=1)
+		so1 = make_sales_order(item_code=item, qty=1)
 
 		pln = frappe.new_doc('Production Plan')
-		pln.company = so.company
+		pln.company = so1.company
 		pln.get_items_from = 'Sales Order'
 		pln.append('sales_orders', {
-			'sales_order': so.name,
-			'sales_order_date': so.transaction_date,
-			'customer': so.customer,
-			'grand_total': so.grand_total
+			'sales_order': so1.name,
+			'sales_order_date': so1.transaction_date,
+			'customer': so1.customer,
+			'grand_total': so1.grand_total
 		})
-		so = make_sales_order(item_code=item, qty=2)
+		so2 = make_sales_order(item_code=item, qty=2)
 		pln.append('sales_orders', {
-			'sales_order': so.name,
-			'sales_order_date': so.transaction_date,
-			'customer': so.customer,
-			'grand_total': so.grand_total
+			'sales_order': so2.name,
+			'sales_order_date': so2.transaction_date,
+			'customer': so2.customer,
+			'grand_total': so2.grand_total
 		})
 		pln.combine_items = 1
 		pln.get_items()
@@ -214,28 +254,37 @@
 			so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty')
 			self.assertEqual(so_wo_qty, 0.0)
 
-		latest_plan = frappe.get_doc('Production Plan', pln.name)
-		latest_plan.cancel()
+		pln.reload()
+		pln.cancel()
 
 	def test_pp_to_mr_customer_provided(self):
-		#Material Request from Production Plan for Customer Provided
+		" Test Material Request from Production Plan for Customer Provided Item."
 		create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
 		create_item('Production Item CUST')
+
 		for item, raw_materials in {'Production Item CUST': ['Raw Material Item 1', 'CUST-0987']}.items():
 			if not frappe.db.get_value('BOM', {'item': item}):
 				make_bom(item = item, raw_materials = raw_materials)
 		production_plan = create_production_plan(item_code = 'Production Item CUST')
 		production_plan.make_material_request()
-		material_request = frappe.db.get_value('Material Request Item', {'production_plan': production_plan.name, 'item_code': 'CUST-0987'}, 'parent')
+
+		material_request = frappe.db.get_value(
+			'Material Request Item',
+			{'production_plan': production_plan.name, 'item_code': 'CUST-0987'},
+			'parent'
+		)
 		mr = frappe.get_doc('Material Request', material_request)
+
 		self.assertTrue(mr.material_request_type, 'Customer Provided')
 		self.assertTrue(mr.customer, '_Test Customer')
 
 	def test_production_plan_with_multi_level_bom(self):
-		#|Item Code			|	Qty	|
-		#|Test BOM 1	 		|	1	|
-		#|	Test BOM 2		|	2	|
-		#|		Test BOM 3	|	3	|
+		"""
+		Item Code	|	Qty	|
+		|Test BOM 1	|	1	|
+		|Test BOM 2	|	2	|
+		|Test BOM 3	|	3	|
+		"""
 
 		for item_code in ["Test BOM 1", "Test BOM 2", "Test BOM 3", "Test RM BOM 1"]:
 			create_item(item_code, is_stock_item=1)
@@ -264,15 +313,18 @@
 		pln.make_work_order()
 
 		#last level sub-assembly work order produce qty
-		to_produce_qty = frappe.db.get_value("Work Order",
-			{"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty")
+		to_produce_qty = frappe.db.get_value(
+			"Work Order",
+			{"production_plan": pln.name, "production_item": "Test BOM 3"},
+			"qty"
+		)
 
 		self.assertEqual(to_produce_qty, 18.0)
 		pln.cancel()
 		frappe.delete_doc("Production Plan", pln.name)
 
 	def test_get_warehouse_list_group(self):
-		"""Check if required warehouses are returned"""
+		"Check if required child warehouses are returned."
 		warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]'
 
 		warehouses = set(get_warehouse_list(warehouse_json))
@@ -284,6 +336,7 @@
 				msg=f"Following warehouses were expected {', '.join(missing_warehouse)}")
 
 	def test_get_warehouse_list_single(self):
+		"Check if same warehouse is returned in absence of child warehouses."
 		warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]'
 
 		warehouses = set(get_warehouse_list(warehouse_json))
@@ -292,6 +345,7 @@
 		self.assertEqual(warehouses, expected_warehouses)
 
 	def test_get_sales_order_with_variant(self):
+		"Check if Template BOM is fetched in absence of Variant BOM."
 		rm_item = create_item('PIV_RM', valuation_rate = 100)
 		if not frappe.db.exists('Item', {"item_code": 'PIV'}):
 			item = create_item('PIV', valuation_rate = 100)
@@ -348,7 +402,7 @@
 		frappe.db.rollback()
 
 	def test_subassmebly_sorting(self):
-		""" Test subassembly sorting in case of multiple items with nested BOMs"""
+		"Test subassembly sorting in case of multiple items with nested BOMs."
 		from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
 
 		prefix = "_TestLevel_"
@@ -386,6 +440,7 @@
 		self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item)
 
 	def test_multiple_work_order_for_production_plan_item(self):
+		"Test producing Prod Plan (making WO) in parts."
 		def create_work_order(item, pln, qty):
 			# Get Production Items
 			items_data = pln.get_production_items()
@@ -441,7 +496,107 @@
 		pln.reload()
 		self.assertEqual(pln.po_items[0].ordered_qty, 0)
 
+	def test_production_plan_pending_qty_with_sales_order(self):
+		"""
+		Test Prod Plan impact via: SO -> Prod Plan -> WO -> SE -> SE (cancel)
+		"""
+		from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
+		from erpnext.manufacturing.doctype.work_order.work_order import (
+			make_stock_entry as make_se_from_wo,
+		)
+
+		make_stock_entry(item_code="Raw Material Item 1",
+			target="Work In Progress - _TC",
+			qty=2, basic_rate=100
+		)
+		make_stock_entry(item_code="Raw Material Item 2",
+			target="Work In Progress - _TC",
+			qty=2, basic_rate=100
+		)
+
+		item = 'Test Production Item 1'
+		so = make_sales_order(item_code=item, qty=1)
+
+		pln = create_production_plan(
+			company=so.company,
+			get_items_from="Sales Order",
+			sales_order=so,
+			skip_getting_mr_items=True
+		)
+		self.assertEqual(pln.po_items[0].pending_qty, 1)
+
+		wo = make_wo_order_test_record(
+			item_code=item, qty=1,
+			company=so.company,
+			wip_warehouse='Work In Progress - _TC',
+			fg_warehouse='Finished Goods - _TC',
+			skip_transfer=1,
+			do_not_submit=True
+		)
+		wo.production_plan = pln.name
+		wo.production_plan_item = pln.po_items[0].name
+		wo.submit()
+
+		se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1))
+		se.submit()
+
+		pln.reload()
+		self.assertEqual(pln.po_items[0].pending_qty, 0)
+
+		se.cancel()
+		pln.reload()
+		self.assertEqual(pln.po_items[0].pending_qty, 1)
+
+	def test_production_plan_pending_qty_independent_items(self):
+		"Test Prod Plan impact if items are added independently (no from SO or MR)."
+		from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
+		from erpnext.manufacturing.doctype.work_order.work_order import (
+			make_stock_entry as make_se_from_wo,
+		)
+
+		make_stock_entry(item_code="Raw Material Item 1",
+			target="Work In Progress - _TC",
+			qty=2, basic_rate=100
+		)
+		make_stock_entry(item_code="Raw Material Item 2",
+			target="Work In Progress - _TC",
+			qty=2, basic_rate=100
+		)
+
+		pln = create_production_plan(
+			item_code='Test Production Item 1',
+			skip_getting_mr_items=True
+		)
+		self.assertEqual(pln.po_items[0].pending_qty, 1)
+
+		wo = make_wo_order_test_record(
+			item_code='Test Production Item 1', qty=1,
+			company=pln.company,
+			wip_warehouse='Work In Progress - _TC',
+			fg_warehouse='Finished Goods - _TC',
+			skip_transfer=1,
+			do_not_submit=True
+		)
+		wo.production_plan = pln.name
+		wo.production_plan_item = pln.po_items[0].name
+		wo.submit()
+
+		se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1))
+		se.submit()
+
+		pln.reload()
+		self.assertEqual(pln.po_items[0].pending_qty, 0)
+
+		se.cancel()
+		pln.reload()
+		self.assertEqual(pln.po_items[0].pending_qty, 1)
+
 def create_production_plan(**args):
+	"""
+	sales_order (obj): Sales Order Doc Object
+	get_items_from (str): Sales Order/Material Request
+	skip_getting_mr_items (bool): Whether or not to plan for new MRs
+	"""
 	args = frappe._dict(args)
 
 	pln = frappe.get_doc({
@@ -449,20 +604,35 @@
 		'company': args.company or '_Test Company',
 		'customer': args.customer or '_Test Customer',
 		'posting_date': nowdate(),
-		'include_non_stock_items': args.include_non_stock_items or 1,
-		'include_subcontracted_items': args.include_subcontracted_items or 1,
-		'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 1,
-		'po_items': [{
+		'include_non_stock_items': args.include_non_stock_items or 0,
+		'include_subcontracted_items': args.include_subcontracted_items or 0,
+		'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 0,
+		'get_items_from': 'Sales Order'
+	})
+
+	if not args.get("sales_order"):
+		pln.append('po_items', {
 			'use_multi_level_bom': args.use_multi_level_bom or 1,
 			'item_code': args.item_code,
 			'bom_no': frappe.db.get_value('Item', args.item_code, 'default_bom'),
 			'planned_qty': args.planned_qty or 1,
 			'planned_start_date': args.planned_start_date or now_datetime()
-		}]
-	})
-	mr_items = get_items_for_material_requests(pln.as_dict())
-	for d in mr_items:
-		pln.append('mr_items', d)
+		})
+
+	if args.get("get_items_from") == "Sales Order" and args.get("sales_order"):
+		so = args.get("sales_order")
+		pln.append('sales_orders', {
+			'sales_order': so.name,
+			'sales_order_date': so.transaction_date,
+			'customer': so.customer,
+			'grand_total': so.grand_total
+		})
+		pln.get_items()
+
+	if not args.get("skip_getting_mr_items"):
+		mr_items = get_items_for_material_requests(pln.as_dict())
+		for d in mr_items:
+			pln.append('mr_items', d)
 
 	if not args.do_not_save:
 		pln.insert()
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index a399edd..67c47ef 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -201,6 +201,21 @@
 		self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production),
 			cint(bin1_on_start_production.reserved_qty_for_production))
 
+	def test_reserved_qty_for_production_closed(self):
+
+		wo1 = make_wo_order_test_record(item="_Test FG Item", qty=2,
+			source_warehouse=self.warehouse)
+		item = wo1.required_items[0].item_code
+		bin_before = get_bin(item, self.warehouse)
+		bin_before.update_reserved_qty_for_production()
+
+		make_wo_order_test_record(item="_Test FG Item", qty=2,
+			source_warehouse=self.warehouse)
+		close_work_order(wo1.name, "Closed")
+
+		bin_after = get_bin(item, self.warehouse)
+		self.assertEqual(bin_before.reserved_qty_for_production, bin_after.reserved_qty_for_production)
+
 	def test_backflush_qty_for_overpduction_manufacture(self):
 		cancel_stock_entry = []
 		allow_overproduction("overproduction_percentage_for_work_order", 30)
@@ -703,7 +718,8 @@
 		wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse,
 			company=company)
 
-		self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture')
+		stock_entry = frappe.get_doc(make_stock_entry(wo.name, 'Material Transfer for Manufacture'))
+		self.assertRaises(frappe.ValidationError, stock_entry.save)
 
 	def test_wo_completion_with_pl_bom(self):
 		from erpnext.manufacturing.doctype.bom.test_bom import (
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index a86edfa..ed6a029 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -8,6 +8,8 @@
 from frappe import _
 from frappe.model.document import Document
 from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder import Case
+from frappe.query_builder.functions import Sum
 from frappe.utils import (
 	cint,
 	date_diff,
@@ -74,7 +76,6 @@
 
 		self.set_required_items(reset_only_qty = len(self.get("required_items")))
 
-
 	def validate_sales_order(self):
 		if self.sales_order:
 			self.check_sales_order_on_hold_or_close()
@@ -271,7 +272,7 @@
 
 			produced_qty = total_qty[0][0] if total_qty else 0
 
-		production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item)
+		production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item)
 
 	def before_submit(self):
 		self.create_serial_no_batch_no()
@@ -544,7 +545,7 @@
 				if node.is_bom:
 					operations.extend(_get_operations(node.name, qty=node.exploded_qty))
 
-		bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
+		bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
 		operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
 
 		for correct_index, operation in enumerate(operations, start=1):
@@ -625,7 +626,7 @@
 			frappe.delete_doc("Job Card", d.name)
 
 	def validate_production_item(self):
-		if frappe.db.get_value("Item", self.production_item, "has_variants"):
+		if frappe.get_cached_value("Item", self.production_item, "has_variants"):
 			frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError)
 
 		if self.production_item:
@@ -1175,3 +1176,27 @@
 	doc.set_item_locations()
 
 	return doc
+
+def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float:
+	"""Get total reserved quantity for any item in specified warehouse"""
+	wo = frappe.qb.DocType("Work Order")
+	wo_item = frappe.qb.DocType("Work Order Item")
+
+	return (
+	frappe.qb
+		.from_(wo)
+		.from_(wo_item)
+		.select(Sum(Case()
+				.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
+				.else_(wo_item.required_qty - wo_item.consumed_qty))
+			)
+		.where(
+			(wo_item.item_code == item_code)
+			& (wo_item.parent == wo.name)
+			& (wo.docstatus == 1)
+			& (wo_item.source_warehouse == warehouse)
+			& (wo.status.notin(["Stopped", "Completed", "Closed"]))
+			& ((wo_item.required_qty > wo_item.transferred_qty)
+				| (wo_item.required_qty > wo_item.consumed_qty))
+		)
+	).run()[0][0] or 0.0
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
index 090a3e7..2693352 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
@@ -89,10 +89,10 @@
 			GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
 
 def get_manufacturer_records():
-	details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
+	details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "item_code"])
 	manufacture_details = frappe._dict()
 	for detail in details:
-		dic = manufacture_details.setdefault(detail.get('parent'), {})
+		dic = manufacture_details.setdefault(detail.get('item_code'), {})
 		dic.setdefault('manufacturer', []).append(detail.get('manufacturer'))
 		dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no'))
 
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index c252a58..c6b3159 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -20,4 +20,5 @@
 Loan Management
 Payroll
 Telephony
+Bulk Transaction
 E-commerce
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 7bc7071..4cf7094 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -350,4 +350,8 @@
 erpnext.patches.v14_0.migrate_cost_center_allocations
 erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
 erpnext.patches.v13_0.shopping_cart_to_ecommerce
+erpnext.patches.v13_0.update_disbursement_account
+erpnext.patches.v13_0.update_reserved_qty_closed_wo
+erpnext.patches.v14_0.delete_amazon_mws_doctype
+erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
 erpnext.patches.v14_0.delete_non_profit_doctypes
diff --git a/erpnext/patches/v12_0/rename_mws_settings_fields.py b/erpnext/patches/v12_0/rename_mws_settings_fields.py
deleted file mode 100644
index d5bf38d..0000000
--- a/erpnext/patches/v12_0/rename_mws_settings_fields.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2020, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-import frappe
-
-
-def execute():
-	count = frappe.db.sql("SELECT COUNT(*) FROM `tabSingles` WHERE doctype='Amazon MWS Settings' AND field='enable_sync';")[0][0]
-	if count == 0:
-		frappe.db.sql("UPDATE `tabSingles` SET field='enable_sync' WHERE doctype='Amazon MWS Settings' AND field='enable_synch';")
-
-	frappe.reload_doc("ERPNext Integrations", "doctype", "Amazon MWS Settings")
diff --git a/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py
new file mode 100644
index 0000000..f097ab9
--- /dev/null
+++ b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py
@@ -0,0 +1,36 @@
+import frappe
+
+
+def execute():
+    """
+    1. Get submitted Work Orders with MR, MR Item and SO set
+    2. Get SO Item detail from MR Item detail in WO, and set in WO
+    3. Update work_order_qty in SO
+    """
+    work_order = frappe.qb.DocType("Work Order")
+    query = (
+        frappe.qb.from_(work_order)
+        .select(
+            work_order.name, work_order.produced_qty,
+            work_order.material_request,
+            work_order.material_request_item,
+            work_order.sales_order
+        ).where(
+            (work_order.material_request.isnotnull())
+            & (work_order.material_request_item.isnotnull())
+            & (work_order.sales_order.isnotnull())
+            & (work_order.docstatus == 1)
+            & (work_order.produced_qty > 0)
+        )
+    )
+    results = query.run(as_dict=True)
+
+    for row in results:
+        so_item = frappe.get_value(
+            "Material Request Item", row.material_request_item, "sales_order_item"
+        )
+        frappe.db.set_value("Work Order", row.name, "sales_order_item", so_item)
+
+        if so_item:
+            wo = frappe.get_doc("Work Order", row.name)
+            wo.update_work_order_qty_in_so()
diff --git a/erpnext/patches/v13_0/update_disbursement_account.py b/erpnext/patches/v13_0/update_disbursement_account.py
new file mode 100644
index 0000000..c56fa8f
--- /dev/null
+++ b/erpnext/patches/v13_0/update_disbursement_account.py
@@ -0,0 +1,22 @@
+import frappe
+
+
+def execute():
+
+	frappe.reload_doc("loan_management", "doctype", "loan_type")
+	frappe.reload_doc("loan_management", "doctype", "loan")
+
+	loan_type = frappe.qb.DocType("Loan Type")
+	loan = frappe.qb.DocType("Loan")
+
+	frappe.qb.update(
+		loan_type
+	).set(
+		loan_type.disbursement_account, loan_type.payment_account
+	).run()
+
+	frappe.qb.update(
+		loan
+	).set(
+		loan.disbursement_account, loan.payment_account
+	).run()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py
new file mode 100644
index 0000000..00926b0
--- /dev/null
+++ b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py
@@ -0,0 +1,28 @@
+import frappe
+
+from erpnext.stock.utils import get_bin
+
+
+def execute():
+
+	wo = frappe.qb.DocType("Work Order")
+	wo_item = frappe.qb.DocType("Work Order Item")
+
+	incorrect_item_wh = (
+		frappe.qb
+			.from_(wo)
+			.join(wo_item).on(wo.name == wo_item.parent)
+			.select(wo_item.item_code, wo.source_warehouse).distinct()
+			.where(
+				(wo.status == "Closed")
+				& (wo.docstatus == 1)
+				& (wo.source_warehouse.notnull())
+			)
+	).run()
+
+	for item_code, warehouse in incorrect_item_wh:
+		if not (item_code and warehouse):
+			continue
+
+		bin = get_bin(item_code, warehouse)
+		bin.update_reserved_qty_for_production()
diff --git a/erpnext/patches/v14_0/delete_amazon_mws_doctype.py b/erpnext/patches/v14_0/delete_amazon_mws_doctype.py
new file mode 100644
index 0000000..525da6c
--- /dev/null
+++ b/erpnext/patches/v14_0/delete_amazon_mws_doctype.py
@@ -0,0 +1,5 @@
+import frappe
+
+
+def execute():
+	frappe.delete_doc("DocType", "Amazon MWS Settings", ignore_missing=True)
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js
index d4f7c9c..3d69c46 100644
--- a/erpnext/payroll/doctype/gratuity/gratuity.js
+++ b/erpnext/payroll/doctype/gratuity/gratuity.js
@@ -3,6 +3,14 @@
 
 frappe.ui.form.on('Gratuity', {
 	setup: function (frm) {
+		frm.set_query("salary_component", function () {
+			return {
+				filters: {
+					type: "Earning"
+				}
+			};
+		});
+
 		frm.set_query("expense_account", function () {
 			return {
 				filters: {
@@ -24,7 +32,7 @@
 		});
 	},
 	refresh: function (frm) {
-		if (frm.doc.docstatus == 1 && frm.doc.status == "Unpaid") {
+		if (frm.doc.docstatus == 1 && !frm.doc.pay_via_salary_slip && frm.doc.status == "Unpaid") {
 			frm.add_custom_button(__("Create Payment Entry"), function () {
 				return frappe.call({
 					method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json
index 1970895..1fd1cec 100644
--- a/erpnext/payroll/doctype/gratuity/gratuity.json
+++ b/erpnext/payroll/doctype/gratuity/gratuity.json
@@ -1,7 +1,7 @@
 {
  "actions": [],
  "autoname": "HR-GRA-PAY-.#####",
- "creation": "2020-08-05 20:52:13.024683",
+ "creation": "2022-01-27 16:24:28.200061",
  "doctype": "DocType",
  "editable_grid": 1,
  "engine": "InnoDB",
@@ -16,6 +16,9 @@
   "company",
   "gratuity_rule",
   "section_break_5",
+  "pay_via_salary_slip",
+  "payroll_date",
+  "salary_component",
   "payable_account",
   "expense_account",
   "mode_of_payment",
@@ -78,18 +81,20 @@
    "reqd": 1
   },
   {
+   "depends_on": "eval: !doc.pay_via_salary_slip",
    "fieldname": "expense_account",
    "fieldtype": "Link",
    "label": "Expense Account",
-   "options": "Account",
-   "reqd": 1
+   "mandatory_depends_on": "eval: !doc.pay_via_salary_slip",
+   "options": "Account"
   },
   {
+   "depends_on": "eval: !doc.pay_via_salary_slip",
    "fieldname": "mode_of_payment",
    "fieldtype": "Link",
    "label": "Mode of Payment",
-   "options": "Mode of Payment",
-   "reqd": 1
+   "mandatory_depends_on": "eval: !doc.pay_via_salary_slip",
+   "options": "Mode of Payment"
   },
   {
    "fieldname": "gratuity_rule",
@@ -151,23 +156,45 @@
    "read_only": 1
   },
   {
+   "depends_on": "eval: !doc.pay_via_salary_slip",
    "fieldname": "payable_account",
    "fieldtype": "Link",
    "label": "Payable Account",
-   "options": "Account",
-   "reqd": 1
+   "mandatory_depends_on": "eval: !doc.pay_via_salary_slip",
+   "options": "Account"
   },
   {
    "fieldname": "cost_center",
    "fieldtype": "Link",
    "label": "Cost Center",
    "options": "Cost Center"
+  },
+  {
+   "default": "1",
+   "fieldname": "pay_via_salary_slip",
+   "fieldtype": "Check",
+   "label": "Pay via Salary Slip"
+  },
+  {
+   "depends_on": "pay_via_salary_slip",
+   "fieldname": "payroll_date",
+   "fieldtype": "Date",
+   "label": "Payroll Date",
+   "mandatory_depends_on": "pay_via_salary_slip"
+  },
+  {
+   "depends_on": "pay_via_salary_slip",
+   "fieldname": "salary_component",
+   "fieldtype": "Link",
+   "label": "Salary Component",
+   "mandatory_depends_on": "pay_via_salary_slip",
+   "options": "Salary Component"
   }
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-01-19 12:54:37.306145",
+ "modified": "2022-02-02 14:00:45.536152",
  "modified_by": "Administrator",
  "module": "Payroll",
  "name": "Gratuity",
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py
index 476990a..939634a 100644
--- a/erpnext/payroll/doctype/gratuity/gratuity.py
+++ b/erpnext/payroll/doctype/gratuity/gratuity.py
@@ -21,7 +21,10 @@
 			self.status = "Unpaid"
 
 	def on_submit(self):
-		self.create_gl_entries()
+		if self.pay_via_salary_slip:
+			self.create_additional_salary()
+		else:
+			self.create_gl_entries()
 
 	def on_cancel(self):
 		self.ignore_linked_doctypes = ['GL Entry']
@@ -64,6 +67,19 @@
 
 		return gl_entry
 
+	def create_additional_salary(self):
+		if self.pay_via_salary_slip:
+			additional_salary = frappe.new_doc('Additional Salary')
+			additional_salary.employee = self.employee
+			additional_salary.salary_component = self.salary_component
+			additional_salary.overwrite_salary_structure_amount = 0
+			additional_salary.amount = self.amount
+			additional_salary.payroll_date = self.payroll_date
+			additional_salary.company = self.company
+			additional_salary.ref_doctype = self.doctype
+			additional_salary.ref_docname = self.name
+			additional_salary.submit()
+
 	def set_total_advance_paid(self):
 		paid_amount = frappe.db.sql("""
 			select ifnull(sum(debit_in_account_currency), 0) as paid_amount
diff --git a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py
index aeadba1..771a6fe 100644
--- a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py
+++ b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py
@@ -10,7 +10,7 @@
 		'transactions': [
 			{
 				'label': _('Payment'),
-				'items': ['Payment Entry']
+				'items': ['Payment Entry', 'Additional Salary']
 			}
 		]
 	}
diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py
index 93cba06..90e8061 100644
--- a/erpnext/payroll/doctype/gratuity/test_gratuity.py
+++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py
@@ -18,27 +18,25 @@
 
 test_dependencies = ["Salary Component", "Salary Slip", "Account"]
 class TestGratuity(unittest.TestCase):
-	@classmethod
-	def setUpClass(cls):
+	def setUp(self):
+		frappe.db.delete("Gratuity")
+		frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
+
 		make_earning_salary_component(setup=True, test_tax=True, company_list=['_Test Company'])
 		make_deduction_salary_component(setup=True, test_tax=True, company_list=['_Test Company'])
 
-	def setUp(self):
-		frappe.db.sql("DELETE FROM `tabGratuity`")
-
 	def test_get_last_salary_slip_should_return_none_for_new_employee(self):
 		new_employee = make_employee("new_employee@salary.com", company='_Test Company')
 		salary_slip = get_last_salary_slip(new_employee)
 		assert salary_slip is None
 
-	def test_check_gratuity_amount_based_on_current_slab(self):
+	def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self):
 		employee, sal_slip = create_employee_and_get_last_salary_slip()
 
 		rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
+		gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name)
 
-		gratuity = create_gratuity(employee=employee, rule=rule.name)
-
-		#work experience calculation
+		# work experience calculation
 		date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date'])
 		employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days
 
@@ -64,6 +62,9 @@
 
 		self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
 
+		# additional salary creation (Pay via salary slip)
+		self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name}))
+
 	def test_check_gratuity_amount_based_on_all_previous_slabs(self):
 		employee, sal_slip = create_employee_and_get_last_salary_slip()
 		rule = get_gratuity_rule("Rule Under Limited Contract (UAE)")
@@ -117,8 +118,8 @@
 		self.assertEqual(flt(gratuity.paid_amount,2), flt(gratuity.amount, 2))
 
 	def tearDown(self):
-		frappe.db.sql("DELETE FROM `tabGratuity`")
-		frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'")
+		frappe.db.rollback()
+
 
 def get_gratuity_rule(name):
 	rule = frappe.db.exists("Gratuity Rule", name)
@@ -141,9 +142,14 @@
 	gratuity.employee = args.employee
 	gratuity.posting_date = getdate()
 	gratuity.gratuity_rule = args.rule or "Rule Under Limited Contract (UAE)"
-	gratuity.expense_account = args.expense_account or 'Payment Account - _TC'
-	gratuity.payable_account = args.payable_account or get_payable_account("_Test Company")
-	gratuity.mode_of_payment = args.mode_of_payment or 'Cash'
+	gratuity.pay_via_salary_slip = args.pay_via_salary_slip or 0
+	if gratuity.pay_via_salary_slip:
+		gratuity.payroll_date = getdate()
+		gratuity.salary_component = "Performance Bonus"
+	else:
+		gratuity.expense_account = args.expense_account or 'Payment Account - _TC'
+		gratuity.payable_account = args.payable_account or get_payable_account("_Test Company")
+		gratuity.mode_of_payment = args.mode_of_payment or 'Cash'
 
 	gratuity.save()
 	gratuity.submit()
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index db88c06..a634dfe 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -527,11 +527,12 @@
 		""" % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True)
 
 def remove_payrolled_employees(emp_list, start_date, end_date):
+	new_emp_list = []
 	for employee_details in emp_list:
-		if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}):
-			emp_list.remove(employee_details)
+		if not frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}):
+			new_emp_list.append(employee_details)
 
-	return emp_list
+	return new_emp_list
 
 @frappe.whitelist()
 def get_start_end_dates(payroll_frequency, start_date=None, company=None):
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index 4f097fa..3b7f4b2 100644
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
@@ -124,7 +124,7 @@
 
 		if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"):
 				create_account(account_name="_Test Payroll Payable",
-					company="_Test Company", parent_account="Current Liabilities - _TC")
+					company="_Test Company", parent_account="Current Liabilities - _TC", account_type="Payable")
 
 		if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \
 			frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC":
@@ -214,6 +214,7 @@
 			create_loan_type("Car Loan", 500000, 8.4,
 				is_term_loan=1,
 				mode_of_payment='Cash',
+				disbursement_account='Disbursement Account - _TC',
 				payment_account='Payment Account - _TC',
 				loan_account='Loan Account - _TC',
 				interest_income_account='Interest Income Account - _TC',
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 597fd5a..daa0f89 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -6,6 +6,7 @@
 import unittest
 
 import frappe
+from frappe.model.document import Document
 from frappe.utils import (
 	add_days,
 	add_months,
@@ -370,6 +371,7 @@
 		create_loan_type("Car Loan", 500000, 8.4,
 			is_term_loan=1,
 			mode_of_payment='Cash',
+			disbursement_account='Disbursement Account - _TC',
 			payment_account='Payment Account - _TC',
 			loan_account='Loan Account - _TC',
 			interest_income_account='Interest Income Account - _TC',
@@ -686,20 +688,25 @@
 
 def make_salary_component(salary_components, test_tax, company_list=None):
 	for salary_component in salary_components:
-		if not frappe.db.exists('Salary Component', salary_component["salary_component"]):
-			if test_tax:
-				if salary_component["type"] == "Earning":
-					salary_component["is_tax_applicable"] = 1
-				elif salary_component["salary_component"] == "TDS":
-					salary_component["variable_based_on_taxable_salary"] = 1
-					salary_component["amount_based_on_formula"] = 0
-					salary_component["amount"] = 0
-					salary_component["formula"] = ""
-					salary_component["condition"] = ""
-			salary_component["doctype"] = "Salary Component"
-			salary_component["salary_component_abbr"] = salary_component["abbr"]
-			frappe.get_doc(salary_component).insert()
-		get_salary_component_account(salary_component["salary_component"], company_list)
+		if frappe.db.exists('Salary Component', salary_component["salary_component"]):
+			continue
+
+		if test_tax:
+			if salary_component["type"] == "Earning":
+				salary_component["is_tax_applicable"] = 1
+			elif salary_component["salary_component"] == "TDS":
+				salary_component["variable_based_on_taxable_salary"] = 1
+				salary_component["amount_based_on_formula"] = 0
+				salary_component["amount"] = 0
+				salary_component["formula"] = ""
+				salary_component["condition"] = ""
+
+		salary_component["salary_component_abbr"] = salary_component["abbr"]
+		doc = frappe.new_doc("Salary Component")
+		doc.update(salary_component)
+		doc.insert()
+
+		get_salary_component_account(doc, company_list)
 
 def get_salary_component_account(sal_comp, company_list=None):
 	company = erpnext.get_default_company()
@@ -707,7 +714,9 @@
 	if company_list and company not in company_list:
 		company_list.append(company)
 
-	sal_comp = frappe.get_doc("Salary Component", sal_comp)
+	if not isinstance(sal_comp, Document):
+		sal_comp = frappe.get_doc("Salary Component", sal_comp)
+
 	if not sal_comp.get("accounts"):
 		for d in company_list:
 			company_abbr = frappe.get_cached_value('Company', d, 'abbr')
@@ -725,7 +734,7 @@
 			})
 			sal_comp.save()
 
-def create_account(account_name, company, parent_account):
+def create_account(account_name, company, parent_account, account_type=None):
 	company_abbr = frappe.get_cached_value('Company',  company,  'abbr')
 	account = frappe.db.get_value("Account", account_name + " - " + company_abbr)
 	if not account:
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..aa3e2f3 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1463,7 +1463,8 @@
 						"item_code": d.item_code,
 						"pricing_rules": d.pricing_rules,
 						"parenttype": d.parenttype,
-						"parent": d.parent
+						"parent": d.parent,
+						"price_list_rate": d.price_list_rate
 					})
 				}
 			});
@@ -2288,7 +2289,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/regional/india/utils.py b/erpnext/regional/india/utils.py
index 8715ef5..d443f9c 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -219,7 +219,6 @@
 
 	if not party_details.place_of_supply: return party_details
 	if not party_details.company_gstin: return party_details
-	if not party_details.supplier_gstin: return party_details
 
 	if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
 		and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js
index 4124e3d..03c729e 100644
--- a/erpnext/regional/report/datev/datev.js
+++ b/erpnext/regional/report/datev/datev.js
@@ -40,7 +40,11 @@
 		});
 
 		query_report.page.add_menu_item(__("Download DATEV File"), () => {
-			const filters = JSON.stringify(query_report.get_values());
+			const filters = encodeURIComponent(
+				JSON.stringify(
+					query_report.get_values()
+				)
+			);
 			window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`);
 		});
 
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index e50ff18..ce2ffb4 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -28,7 +28,7 @@
 			posting_date,
 			base_grand_total,
 			base_rounded_total,
-			COALESCE(NULLIF(customer_gstin,''), NULLIF(billing_address_gstin, '')) as customer_gstin,
+			NULLIF(billing_address_gstin, '') as billing_address_gstin,
 			place_of_supply,
 			ecommerce_gstin,
 			reverse_charge,
@@ -259,7 +259,7 @@
 
 
 		if self.filters.get("type_of_business") ==  "B2B":
-			conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1"
+			conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Registered Composition', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1"
 
 		if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
 			b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
@@ -383,7 +383,7 @@
 		for invoice, items in self.invoice_items.items():
 			if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \
 				and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \
-				and self.invoices.get(invoice, {}).get('gst_category') == "Overseas":
+				and self.invoices.get(invoice, {}).get('gst_category') in ("Overseas", "SEZ"):
 					self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
 
 	def get_columns(self):
@@ -409,7 +409,7 @@
 		if self.filters.get("type_of_business") ==  "B2B":
 			self.invoice_columns = [
 				{
-					"fieldname": "customer_gstin",
+					"fieldname": "billing_address_gstin",
 					"label": "GSTIN/UIN of Recipient",
 					"fieldtype": "Data",
 					"width": 150
@@ -516,7 +516,7 @@
 		elif self.filters.get("type_of_business") == "CDNR-REG":
 			self.invoice_columns = [
 				{
-					"fieldname": "customer_gstin",
+					"fieldname": "billing_address_gstin",
 					"label": "GSTIN/UIN of Recipient",
 					"fieldtype": "Data",
 					"width": 150
@@ -817,7 +817,7 @@
 	res = {}
 	if filters["type_of_business"] == "B2B":
 		for item in report_data[:-1]:
-			res.setdefault(item["customer_gstin"], {}).setdefault(item["invoice_number"],[]).append(item)
+			res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item)
 
 		out = get_b2b_json(res, gstin)
 		gst_json["b2b"] = out
@@ -841,7 +841,7 @@
 		gst_json["exp"] = out
 	elif filters["type_of_business"] == "CDNR-REG":
 		for item in report_data[:-1]:
-			res.setdefault(item["customer_gstin"], {}).setdefault(item["invoice_number"],[]).append(item)
+			res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item)
 
 		out = get_cdnr_reg_json(res, gstin)
 		gst_json["cdnr"] = out
@@ -875,7 +875,7 @@
 	}
 
 def get_b2b_json(res, gstin):
-	inv_type, out = {"Registered Regular": "R", "Deemed Export": "DE", "URD": "URD", "SEZ": "SEZ"}, []
+	out = []
 	for gst_in in res:
 		b2b_item, inv = {"ctin": gst_in, "inv": []}, []
 		if not gst_in: continue
@@ -889,7 +889,7 @@
 			inv_item = get_basic_invoice_detail(invoice[0])
 			inv_item["pos"] = "%02d" % int(invoice[0]["place_of_supply"].split('-')[0])
 			inv_item["rchrg"] = invoice[0]["reverse_charge"]
-			inv_item["inv_typ"] = inv_type.get(invoice[0].get("gst_category", ""),"")
+			inv_item["inv_typ"] = get_invoice_type(invoice[0])
 
 			if inv_item["pos"]=="00": continue
 			inv_item["itms"] = []
@@ -1044,7 +1044,7 @@
 				"ntty": invoice[0]["document_type"],
 				"pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]),
 				"rchrg": invoice[0]["reverse_charge"],
-				"inv_typ": get_invoice_type_for_cdnr(invoice[0])
+				"inv_typ": get_invoice_type(invoice[0])
 			}
 
 			inv_item["itms"] = []
@@ -1069,7 +1069,7 @@
 			"val": abs(flt(items[0]["invoice_value"])),
 			"ntty": items[0]["document_type"],
 			"pos": "%02d" % int(items[0]["place_of_supply"].split('-')[0]),
-			"typ": get_invoice_type_for_cdnrur(items[0])
+			"typ": get_invoice_type(items[0])
 		}
 
 		inv_item["itms"] = []
@@ -1110,29 +1110,21 @@
 
 	return out
 
-def get_invoice_type_for_cdnr(row):
-	if row.get('gst_category') == 'SEZ':
-		if row.get('export_type') == 'WPAY':
-			invoice_type = 'SEWP'
-		else:
-			invoice_type = 'SEWOP'
-	elif row.get('gst_category') == 'Deemed Export':
-		invoice_type = 'DE'
-	elif row.get('gst_category') == 'Registered Regular':
-		invoice_type = 'R'
+def get_invoice_type(row):
+	gst_category = row.get('gst_category')
 
-	return invoice_type
+	if gst_category == 'SEZ':
+		return 'SEWP' if row.get('export_type') == 'WPAY' else 'SEWOP'
 
-def get_invoice_type_for_cdnrur(row):
-	if row.get('gst_category') == 'Overseas':
-		if row.get('export_type') == 'WPAY':
-			invoice_type = 'EXPWP'
-		else:
-			invoice_type = 'EXPWOP'
-	elif row.get('gst_category') == 'Unregistered':
-		invoice_type = 'B2CL'
+	if gst_category == 'Overseas':
+		return 'EXPWP' if row.get('export_type') == 'WPAY' else 'EXPWOP'
 
-	return invoice_type
+	return ({
+		'Deemed Export': 'DE',
+		'Registered Regular': 'R',
+		'Registered Composition': 'R',
+		'Unregistered': 'B2CL'
+	}).get(gst_category)
 
 def get_basic_invoice_detail(row):
 	return {
@@ -1154,7 +1146,7 @@
 	# calculate tax amount added
 	tax = flt((row["taxable_value"]*rate)/100.0, 2)
 	frappe.errprint([tax, tax/2])
-	if row.get("customer_gstin") and gstin[0:2] == row["customer_gstin"][0:2]:
+	if row.get("billing_address_gstin") and gstin[0:2] == row["billing_address_gstin"][0:2]:
 		itm_det.update({"camt": flt(tax/2.0, 2), "samt": flt(tax/2.0, 2)})
 	else:
 		itm_det.update({"iamt": tax})
@@ -1199,4 +1191,4 @@
 	if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]:
 		return True
 	else:
-		return False
\ No newline at end of file
+		return False
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/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index acf048e..73c5bd2 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -6,7 +6,7 @@
 import frappe
 import frappe.permissions
 from frappe.core.doctype.user_permission.test_user_permission import create_user
-from frappe.utils import add_days, flt, getdate, nowdate
+from frappe.utils import add_days, flt, getdate, nowdate, today
 
 from erpnext.controllers.accounts_controller import update_child_qty_rate
 from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
@@ -1399,6 +1399,48 @@
 		so.load_from_db()
 		self.assertEqual(so.billing_status, 'Fully Billed')
 
+	def test_so_back_updated_from_wo_via_mr(self):
+		"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
+		from erpnext.manufacturing.doctype.work_order.work_order import (
+			make_stock_entry as make_se_from_wo,
+		)
+		from erpnext.stock.doctype.material_request.material_request import raise_work_orders
+
+		so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}])
+
+		mr = make_material_request(so.name)
+		mr.material_request_type = "Manufacture"
+		mr.schedule_date = today()
+		mr.submit()
+
+		# WO from MR
+		wo_name = raise_work_orders(mr.name)[0]
+		wo = frappe.get_doc("Work Order", wo_name)
+		wo.wip_warehouse = "Work In Progress - _TC"
+		wo.skip_transfer = True
+
+		self.assertEqual(wo.sales_order, so.name)
+		self.assertEqual(wo.sales_order_item, so.items[0].name)
+
+		wo.submit()
+		make_stock_entry(item_code="_Test Item", # Stock RM
+			target="Work In Progress - _TC",
+			qty=4, basic_rate=100
+		)
+		make_stock_entry(item_code="_Test Item Home Desktop 100", # Stock RM
+			target="Work In Progress - _TC",
+			qty=4, basic_rate=100
+		)
+
+		se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 2))
+		se.submit() # Finish WO
+
+		mr.reload()
+		wo.reload()
+		so.reload()
+		self.assertEqual(so.items[0].work_order_qty, wo.produced_qty)
+		self.assertEqual(mr.status, "Manufactured")
+
 def automatically_fetch_payment_terms(enable=1):
 	accounts_settings = frappe.get_doc("Accounts Settings")
 	accounts_settings.automatically_fetch_payment_terms = enable
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 27bc541..7c4a3f6 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -80,7 +80,7 @@
    "description": "How often should Project and Company be updated based on Sales Transactions?",
    "fieldname": "sales_update_frequency",
    "fieldtype": "Select",
-   "label": "Sales Update Frequency",
+   "label": "Sales Update Frequency in Company and Project",
    "options": "Each Transaction\nDaily\nMonthly",
    "reqd": 1
   },
@@ -171,7 +171,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-09-13 12:32:17.004404",
+ "modified": "2022-02-04 15:41:59.939261",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Selling Settings",
@@ -189,5 +189,6 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/accounts/print_format/gst_pos_invoice/__init__.py b/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py
similarity index 100%
copy from erpnext/accounts/print_format/gst_pos_invoice/__init__.py
copy to erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
new file mode 100644
index 0000000..0e36b3f
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
@@ -0,0 +1,84 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+function get_filters() {
+	let filters = [
+		{
+			"fieldname":"company",
+			"label": __("Company"),
+			"fieldtype": "Link",
+			"options": "Company",
+			"default": frappe.defaults.get_user_default("Company"),
+			"reqd": 1
+		},
+		{
+			"fieldname":"period_start_date",
+			"label": __("Start Date"),
+			"fieldtype": "Date",
+			"reqd": 1,
+			"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
+		},
+		{
+			"fieldname":"period_end_date",
+			"label": __("End Date"),
+			"fieldtype": "Date",
+			"reqd": 1,
+			"default": frappe.datetime.get_today()
+		},
+		{
+			"fieldname":"sales_order",
+			"label": __("Sales Order"),
+			"fieldtype": "MultiSelectList",
+			"width": 100,
+			"options": "Sales Order",
+			"get_data": function(txt) {
+				return frappe.db.get_link_options("Sales Order", txt, this.filters());
+			},
+			"filters": () => {
+				return {
+					docstatus: 1,
+					payment_terms_template: ['not in', ['']],
+					company: frappe.query_report.get_filter_value("company"),
+					transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]]
+				}
+			},
+			on_change: function(){
+				frappe.query_report.refresh();
+			}
+		}
+	]
+
+	return filters;
+}
+
+frappe.query_reports["Payment Terms Status for Sales Order"] = {
+	"filters": get_filters(),
+	"formatter": function(value, row, column, data, default_formatter){
+		if(column.fieldname == 'invoices' && value) {
+			invoices = value.split(',');
+			const invoice_formatter = (prev_value, curr_value) => {
+				if(prev_value != "") {
+					return prev_value + ", " + default_formatter(curr_value, row, column, data);
+				}
+				else {
+					return default_formatter(curr_value, row, column, data);
+				}
+			}
+			return invoices.reduce(invoice_formatter, "")
+		}
+		else if (column.fieldname == 'paid_amount' && value){
+			formatted_value = default_formatter(value, row, column, data);
+			if(value > 0) {
+				formatted_value = "<span style='color:green;'>" + formatted_value + "</span>"
+			}
+			return formatted_value;
+		}
+		else if (column.fieldname == 'status' && value == 'Completed'){
+			return "<span style='color:green;'>" + default_formatter(value, row, column, data) + "</span>";
+		}
+
+		return default_formatter(value, row, column, data);
+	},
+
+};
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json
new file mode 100644
index 0000000..850fa4d
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json
@@ -0,0 +1,38 @@
+{
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2021-12-28 10:39:34.533964",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-12-30 10:42:06.058457",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Payment Terms Status for Sales Order",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Sales Order",
+ "report_name": "Payment Terms Status for Sales Order",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Sales User"
+  },
+  {
+   "role": "Sales Manager"
+  },
+  {
+   "role": "Maintenance User"
+  },
+  {
+   "role": "Accounts User"
+  },
+  {
+   "role": "Stock User"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
new file mode 100644
index 0000000..e6a56ee
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
@@ -0,0 +1,205 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# License: MIT. See LICENSE
+
+import frappe
+from frappe import _, qb, query_builder
+from frappe.query_builder import functions
+
+
+def get_columns():
+	columns = [
+		{
+			"label": _("Sales Order"),
+			"fieldname": "name",
+			"fieldtype": "Link",
+			"options": "Sales Order",
+		},
+		{
+			"label": _("Posting Date"),
+			"fieldname": "submitted",
+			"fieldtype": "Date",
+		},
+		{
+			"label": _("Payment Term"),
+			"fieldname": "payment_term",
+			"fieldtype": "Data",
+		},
+		{
+			"label": _("Description"),
+			"fieldname": "description",
+			"fieldtype": "Data",
+		},
+		{
+			"label": _("Due Date"),
+			"fieldname": "due_date",
+			"fieldtype": "Date",
+		},
+		{
+			"label": _("Invoice Portion"),
+			"fieldname": "invoice_portion",
+			"fieldtype": "Percent",
+		},
+		{
+			"label": _("Payment Amount"),
+			"fieldname": "base_payment_amount",
+			"fieldtype": "Currency",
+			"options": "currency",
+		},
+		{
+			"label": _("Paid Amount"),
+			"fieldname": "paid_amount",
+			"fieldtype": "Currency",
+			"options": "currency",
+		},
+		{
+			"label": _("Invoices"),
+			"fieldname": "invoices",
+			"fieldtype": "Link",
+			"options": "Sales Invoice",
+		},
+		{
+			"label": _("Status"),
+			"fieldname": "status",
+			"fieldtype": "Data",
+		},
+		{
+			"label": _("Currency"),
+			"fieldname": "currency",
+			"fieldtype": "Currency",
+			"hidden": 1
+		}
+	]
+	return columns
+
+
+def get_conditions(filters):
+	"""
+	Convert filter options to conditions used in query
+	"""
+	filters = frappe._dict(filters) if filters else frappe._dict({})
+	conditions = frappe._dict({})
+
+	conditions.company = filters.company or frappe.defaults.get_user_default("company")
+	conditions.end_date = filters.period_end_date or frappe.utils.today()
+	conditions.start_date = filters.period_start_date or frappe.utils.add_months(
+		conditions.end_date, -1
+	)
+	conditions.sales_order = filters.sales_order or []
+
+	return conditions
+
+
+def get_so_with_invoices(filters):
+	"""
+	Get Sales Order with payment terms template with their associated Invoices
+	"""
+	sorders = []
+
+	so = qb.DocType("Sales Order")
+	ps = qb.DocType("Payment Schedule")
+	datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"])
+	ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
+
+	conditions = get_conditions(filters)
+	query_so = (
+		qb.from_(so)
+		.join(ps)
+		.on(ps.parent == so.name)
+		.select(
+			so.name,
+			so.transaction_date.as_("submitted"),
+			ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
+			ps.payment_term,
+			ps.description,
+			ps.due_date,
+			ps.invoice_portion,
+			ps.base_payment_amount,
+			ps.paid_amount,
+		)
+		.where(
+			(so.docstatus == 1)
+			& (so.payment_terms_template != "NULL")
+			& (so.company == conditions.company)
+			& (so.transaction_date[conditions.start_date : conditions.end_date])
+		)
+		.orderby(so.name, so.transaction_date, ps.due_date)
+	)
+
+	if conditions.sales_order != []:
+		query_so = query_so.where(so.name.isin(conditions.sales_order))
+
+	sorders = query_so.run(as_dict=True)
+
+	invoices = []
+	if sorders != []:
+		soi = qb.DocType("Sales Order Item")
+		si = qb.DocType("Sales Invoice")
+		sii = qb.DocType("Sales Invoice Item")
+		query_inv = (
+			qb.from_(sii)
+			.right_join(si)
+			.on(si.name == sii.parent)
+			.inner_join(soi)
+			.on(soi.name == sii.so_detail)
+			.select(sii.sales_order, sii.parent.as_("invoice"), si.base_grand_total.as_("invoice_amount"))
+			.where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1))
+			.groupby(sii.parent)
+		)
+		invoices = query_inv.run(as_dict=True)
+
+	return sorders, invoices
+
+
+def set_payment_terms_statuses(sales_orders, invoices, filters):
+	"""
+	compute status for payment terms with associated sales invoice using FIFO
+	"""
+
+	for so in sales_orders:
+		so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency')
+		so.invoices = ""
+		for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]:
+			if so.base_payment_amount - so.paid_amount > 0:
+				amount = so.base_payment_amount - so.paid_amount
+				if inv.invoice_amount >= amount:
+					inv.invoice_amount -= amount
+					so.paid_amount += amount
+					so.invoices += "," + inv.invoice
+					so.status = "Completed"
+					break
+				else:
+					so.paid_amount += inv.invoice_amount
+					inv.invoice_amount = 0
+					so.invoices += "," + inv.invoice
+					so.status = "Partly Paid"
+
+	return sales_orders, invoices
+
+
+def prepare_chart(s_orders):
+	if len(set([x.name for x in s_orders])) == 1:
+		chart = {
+			"data": {
+				"labels": [term.payment_term for term in s_orders],
+				"datasets": [
+					{"name": "Payment Amount", "values": [x.base_payment_amount for x in s_orders],},
+					{"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],},
+				],
+			},
+			"type": "bar",
+		}
+		return chart
+
+
+def execute(filters=None):
+	columns = get_columns()
+	sales_orders, so_invoices = get_so_with_invoices(filters)
+	sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices, filters)
+
+	prepare_chart(sales_orders)
+
+	data = sales_orders
+	message = []
+	chart = prepare_chart(sales_orders)
+
+	return columns, data, message, chart
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
new file mode 100644
index 0000000..cad41e1
--- /dev/null
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
@@ -0,0 +1,198 @@
+import datetime
+
+import frappe
+from frappe.utils import add_days
+
+from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import (
+	execute,
+)
+from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.tests.utils import ERPNextTestCase
+
+test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"]
+
+
+class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
+	def create_payment_terms_template(self):
+		# create template for 50-50 payments
+		template = None
+		if frappe.db.exists("Payment Terms Template", "_Test 50-50"):
+			template = frappe.get_doc("Payment Terms Template", "_Test 50-50")
+		else:
+			template = frappe.get_doc(
+				{
+					"doctype": "Payment Terms Template",
+					"template_name": "_Test 50-50",
+					"terms": [
+						{
+							"doctype": "Payment Terms Template Detail",
+							"due_date_based_on": "Day(s) after invoice date",
+							"payment_term_name": "_Test 50% on 15 Days",
+							"description": "_Test 50-50",
+							"invoice_portion": 50,
+							"credit_days": 15,
+						},
+						{
+							"doctype": "Payment Terms Template Detail",
+							"due_date_based_on": "Day(s) after invoice date",
+							"payment_term_name": "_Test 50% on 30 Days",
+							"description": "_Test 50-50",
+							"invoice_portion": 50,
+							"credit_days": 30,
+						},
+					],
+				}
+			)
+			template.insert()
+		self.template = template
+
+	def test_payment_terms_status(self):
+		self.create_payment_terms_template()
+		item = create_item(item_code="_Test Excavator", is_stock_item=0)
+		so = make_sales_order(
+			transaction_date="2021-06-15",
+			delivery_date=add_days("2021-06-15", -30),
+			item=item.item_code,
+			qty=10,
+			rate=100000,
+			do_not_save=True,
+		)
+		so.po_no = ""
+		so.taxes_and_charges = ""
+		so.taxes = ""
+		so.payment_terms_template = self.template.name
+		so.save()
+		so.submit()
+
+		# make invoice with 60% of the total sales order value
+		sinv = make_sales_invoice(so.name)
+		sinv.taxes_and_charges = ""
+		sinv.taxes = ""
+		sinv.items[0].qty = 6
+		sinv.insert()
+		sinv.submit()
+		columns, data, message, chart = execute(
+			{
+				"company": "_Test Company",
+				"period_start_date": "2021-06-01",
+				"period_end_date": "2021-06-30",
+				"sales_order": [so.name],
+			}
+		)
+
+		expected_value = [
+			{
+				"name": so.name,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Completed",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 6, 30),
+				"invoice_portion": 50.0,
+				"currency": "INR",
+				"base_payment_amount": 500000.0,
+				"paid_amount": 500000.0,
+				"invoices": ","+sinv.name,
+			},
+			{
+				"name": so.name,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Partly Paid",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 7, 15),
+				"invoice_portion": 50.0,
+				"currency": "INR",
+				"base_payment_amount": 500000.0,
+				"paid_amount": 100000.0,
+				"invoices": ","+sinv.name,
+			},
+		]
+		self.assertEqual(data, expected_value)
+
+	def create_exchange_rate(self, date):
+		# make an entry in Currency Exchange list. serves as a static exchange rate
+		if frappe.db.exists({'doctype': "Currency Exchange",'date': date,'from_currency': 'USD', 'to_currency':'INR'}):
+			return
+		else:
+			doc = frappe.get_doc({
+				'doctype': "Currency Exchange",
+				'date': date,
+				'from_currency': 'USD',
+				'to_currency': frappe.get_cached_value("Company", '_Test Company','default_currency'),
+				'exchange_rate': 70,
+				'for_buying': True,
+				'for_selling': True
+			})
+			doc.insert()
+
+	def test_alternate_currency(self):
+		transaction_date = "2021-06-15"
+		self.create_payment_terms_template()
+		self.create_exchange_rate(transaction_date)
+		item = create_item(item_code="_Test Excavator", is_stock_item=0)
+		so = make_sales_order(
+			transaction_date=transaction_date,
+			currency="USD",
+			delivery_date=add_days(transaction_date, -30),
+			item=item.item_code,
+			qty=10,
+			rate=10000,
+			do_not_save=True,
+		)
+		so.po_no = ""
+		so.taxes_and_charges = ""
+		so.taxes = ""
+		so.payment_terms_template = self.template.name
+		so.save()
+		so.submit()
+
+		# make invoice with 60% of the total sales order value
+		sinv = make_sales_invoice(so.name)
+		sinv.currency = "USD"
+		sinv.taxes_and_charges = ""
+		sinv.taxes = ""
+		sinv.items[0].qty = 6
+		sinv.insert()
+		sinv.submit()
+		columns, data, message, chart = execute(
+			{
+				"company": "_Test Company",
+				"period_start_date": "2021-06-01",
+				"period_end_date": "2021-06-30",
+				"sales_order": [so.name],
+			}
+		)
+
+		# report defaults to company currency.
+		expected_value = [
+			{
+				"name": so.name,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Completed",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 6, 30),
+				"invoice_portion": 50.0,
+				"currency": frappe.get_cached_value("Company", '_Test Company','default_currency'),
+				"base_payment_amount": 3500000.0,
+				"paid_amount": 3500000.0,
+				"invoices": ","+sinv.name,
+			},
+			{
+				"name": so.name,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Partly Paid",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 7, 15),
+				"invoice_portion": 50.0,
+				"currency": frappe.get_cached_value("Company", '_Test Company','default_currency'),
+				"base_payment_amount": 3500000.0,
+				"paid_amount": 700000.0,
+				"invoices": ","+sinv.name,
+			},
+		]
+		self.assertEqual(data, expected_value)
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 16e3847..98131f9 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -227,11 +227,11 @@
 					},
 					callback:function(r){
 						if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
-
 							if (doc.doctype === 'Sales Invoice' && (!doc.update_stock)) return;
-
-							me.set_batch_number(cdt, cdn);
-							me.batch_no(doc, cdt, cdn);
+							if (has_batch_no) {
+								me.set_batch_number(cdt, cdn);
+								me.batch_no(doc, cdt, cdn);
+							}
 						}
 					}
 				});
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index c34e9d0..3bc15a8 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -20,43 +20,12 @@
 			+ flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty)
 			- flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract))
 
-	def get_first_sle(self):
-		sle = frappe.qb.DocType("Stock Ledger Entry")
-		first_sle = (
-				frappe.qb.from_(sle)
-					.select("*")
-					.where((sle.item_code == self.item_code) & (sle.warehouse == self.warehouse))
-					.orderby(sle.posting_date, sle.posting_time, sle.creation)
-					.limit(1)
-				).run(as_dict=True)
-
-		return first_sle and first_sle[0] or None
-
 	def update_reserved_qty_for_production(self):
 		'''Update qty reserved for production from Production Item tables
 			in open work orders'''
+		from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production
 
-		wo = frappe.qb.DocType("Work Order")
-		wo_item = frappe.qb.DocType("Work Order Item")
-
-		self.reserved_qty_for_production = (
-				frappe.qb
-					.from_(wo)
-					.from_(wo_item)
-					.select(Sum(Case()
-							.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
-							.else_(wo_item.required_qty - wo_item.consumed_qty))
-						)
-					.where(
-						(wo_item.item_code == self.item_code)
-						& (wo_item.parent == wo.name)
-						& (wo.docstatus == 1)
-						& (wo_item.source_warehouse == self.warehouse)
-						& (wo.status.notin(["Stopped", "Completed"]))
-						& ((wo_item.required_qty > wo_item.transferred_qty)
-							| (wo_item.required_qty > wo_item.consumed_qty))
-					)
-		).run()[0][0] or 0.0
+		self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse)
 
 		self.set_projected_qty()
 
@@ -126,13 +95,6 @@
 	frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse")
 
 
-def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
-	"""WARNING: This function is deprecated. Inline this function instead of using it."""
-	from erpnext.stock.stock_ledger import repost_current_voucher
-
-	repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
-	update_qty(bin_name, args)
-
 def get_bin_details(bin_name):
 	return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',
 	'reserved_qty', 'indented_qty', 'planned_qty', 'reserved_qty_for_production',
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/item/item.js b/erpnext/stock/doctype/item/item.js
index 2a30ca1..dfc0918 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -545,7 +545,7 @@
 			let selected_attributes = {};
 			me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => {
 				if(i===0) return;
-				let attribute_name = $(col).find('label').html();
+				let attribute_name = $(col).find('label').html().trim();
 				selected_attributes[attribute_name] = [];
 				let checked_opts = $(col).find('.checkbox input');
 				checked_opts.each((i, opt) => {
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index e71cdb3..c797187 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -48,6 +48,7 @@
   "warranty_period",
   "weight_per_unit",
   "weight_uom",
+  "allow_negative_stock",
   "reorder_section",
   "reorder_levels",
   "unit_of_measure_conversion",
@@ -346,7 +347,7 @@
    "fieldname": "valuation_method",
    "fieldtype": "Select",
    "label": "Valuation Method",
-   "options": "\nFIFO\nMoving Average"
+   "options": "\nFIFO\nMoving Average\nLIFO"
   },
   {
    "depends_on": "is_stock_item",
@@ -907,6 +908,12 @@
    "fieldname": "is_grouped_asset",
    "fieldtype": "Check",
    "label": "Create Grouped Asset"
+  },
+  {
+   "default": "0",
+   "fieldname": "allow_negative_stock",
+   "fieldtype": "Check",
+   "label": "Allow Negative Stock"
   }
  ],
  "icon": "fa fa-tag",
@@ -914,7 +921,7 @@
  "image_field": "image",
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2022-01-18 12:57:54.273202",
+ "modified": "2022-02-11 08:07:46.663220",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Item",
@@ -987,4 +994,4 @@
  "states": [],
  "title_field": "item_name",
  "track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index fc45ba9..fd4df42 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -6,6 +6,7 @@
 
 import frappe
 from frappe.test_runner import make_test_objects
+from frappe.utils import add_days, today
 
 from erpnext.controllers.item_variant import (
 	InvalidItemAttributeValueError,
@@ -608,6 +609,45 @@
 		item.item_group = "All Item Groups"
 		item.save()  # if item code saved without item_code then series worked
 
+	@change_settings("Stock Settings", {"allow_negative_stock": 0})
+	def test_item_wise_negative_stock(self):
+		""" When global settings are disabled check that item that allows
+		negative stock can still consume material in all known stock
+		transactions that consume inventory."""
+		from erpnext.stock.stock_ledger import is_negative_stock_allowed
+
+		item = make_item("_TestNegativeItemSetting", {"allow_negative_stock": 1, "valuation_rate": 100})
+		self.assertTrue(is_negative_stock_allowed(item_code=item.name))
+
+		self.consume_item_code_with_differet_stock_transactions(item_code=item.name)
+
+	@change_settings("Stock Settings", {"allow_negative_stock": 0})
+	def test_backdated_negative_stock(self):
+		""" same as test above but backdated entries """
+		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+		item = make_item("_TestNegativeItemSetting", {"allow_negative_stock": 1, "valuation_rate": 100})
+
+		# create a future entry so all new entries are backdated
+		make_stock_entry(qty=1, item_code=item.name, target="_Test Warehouse - _TC", posting_date = add_days(today(), 5))
+		self.consume_item_code_with_differet_stock_transactions(item_code=item.name)
+
+
+	def consume_item_code_with_differet_stock_transactions(self, item_code, warehouse="_Test Warehouse - _TC"):
+		from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+		from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+
+		typical_args = {"item_code": item_code, "warehouse": warehouse}
+
+		create_delivery_note(**typical_args)
+		create_sales_invoice(update_stock=1, **typical_args)
+		make_stock_entry(item_code=item_code, source=warehouse, qty=1, purpose="Material Issue")
+		make_stock_entry(item_code=item_code, source=warehouse, target="Stores - _TC", qty=1)
+		# standalone return
+		make_purchase_receipt(is_return=True, qty=-1, **typical_args)
+
+
 
 def set_item_variant_settings(fields):
 	doc = frappe.get_doc('Item Variant Settings')
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 103e8d6..b39328f 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -533,6 +533,7 @@
 					"stock_uom": d.stock_uom,
 					"expected_delivery_date": d.schedule_date,
 					"sales_order": d.sales_order,
+					"sales_order_item": d.get("sales_order_item"),
 					"bom_no": get_item_details(d.item_code).bom_no,
 					"material_request": mr.name,
 					"material_request_item": d.name,
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/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
index 523ba12..4e472a9 100644
--- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
@@ -9,7 +9,7 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import cint, floor, flt, nowdate
+from frappe.utils import cint, cstr, floor, flt, nowdate
 
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 from erpnext.stock.utils import get_stock_balance
@@ -142,11 +142,44 @@
 	if items_not_accomodated:
 		show_unassigned_items_message(items_not_accomodated)
 
-	items[:] = updated_table if updated_table else items # modify items table
+	if updated_table and _items_changed(items, updated_table, doctype):
+		items[:] = updated_table
+		frappe.msgprint(_("Applied putaway rules."), alert=True)
 
 	if sync and json.loads(sync): # sync with client side
 		return items
 
+def _items_changed(old, new, doctype: str) -> bool:
+	""" Check if any items changed by application of putaway rules.
+
+		If not, changing item table can have side effects since `name` items also changes.
+	"""
+	if len(old) != len(new):
+		return True
+
+	old = [frappe._dict(item) if isinstance(item, dict) else item for item in old]
+
+	if doctype == "Stock Entry":
+		compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no")
+		sort_key = lambda item: (item.item_code, cstr(item.t_warehouse),  # noqa
+				flt(item.transfer_qty), cstr(item.serial_no))
+	else:
+		# purchase receipt / invoice
+		compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no")
+		sort_key = lambda item: (item.item_code, cstr(item.warehouse),  # noqa
+				flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no))
+
+	old_sorted = sorted(old, key=sort_key)
+	new_sorted = sorted(new, key=sort_key)
+
+	# Once sorted by all relevant keys both tables should align if they are same.
+	for old_item, new_item in zip(old_sorted, new_sorted):
+		for key in compare_keys:
+			if old_item.get(key) != new_item.get(key):
+				return True
+	return False
+
+
 def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
 	"""Returns an ordered list of putaway rules to apply on an item."""
 	filters = {
diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
index bd4d811..ff1c19a 100644
--- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
@@ -35,6 +35,18 @@
 			new_uom.uom_name = "Bag"
 			new_uom.save()
 
+	def assertUnchangedItemsOnResave(self, doc):
+		""" Check if same items remain even after reapplication of rules.
+
+			This is required since some business logic like subcontracting
+			depends on `name` of items to be same if item isn't changed.
+		"""
+		doc.reload()
+		old_items = {d.name for d in doc.items}
+		doc.save()
+		new_items = {d.name for d in doc.items}
+		self.assertSetEqual(old_items, new_items)
+
 	def test_putaway_rules_priority(self):
 		"""Test if rule is applied by priority, irrespective of free space."""
 		rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200,
@@ -50,6 +62,8 @@
 		self.assertEqual(pr.items[1].qty, 100)
 		self.assertEqual(pr.items[1].warehouse, self.warehouse_2)
 
+		self.assertUnchangedItemsOnResave(pr)
+
 		pr.delete()
 		rule_1.delete()
 		rule_2.delete()
@@ -162,6 +176,8 @@
 		# leftover space was for 500 kg (0.5 Bag)
 		# Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned
 
+		self.assertUnchangedItemsOnResave(pr)
+
 		pr.delete()
 		rule_1.delete()
 		rule_2.delete()
@@ -196,6 +212,8 @@
 		self.assertEqual(pr.items[1].warehouse, self.warehouse_1)
 		self.assertEqual(pr.items[1].putaway_rule, rule_1.name)
 
+		self.assertUnchangedItemsOnResave(pr)
+
 		pr.delete()
 		rule_1.delete()
 
@@ -239,6 +257,8 @@
 		self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg
 		self.assertEqual(stock_entry_item.putaway_rule, rule_2.name)
 
+		self.assertUnchangedItemsOnResave(stock_entry)
+
 		stock_entry.delete()
 		rule_1.delete()
 		rule_2.delete()
@@ -294,6 +314,8 @@
 		self.assertEqual(stock_entry.items[2].qty, 200)
 		self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name)
 
+		self.assertUnchangedItemsOnResave(stock_entry)
+
 		stock_entry.delete()
 		rule_1.delete()
 		rule_2.delete()
@@ -344,6 +366,8 @@
 		self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:]))
 		self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1")
 
+		self.assertUnchangedItemsOnResave(stock_entry)
+
 		stock_entry.delete()
 		pr.cancel()
 		rule_1.delete()
@@ -366,6 +390,8 @@
 		self.assertEqual(stock_entry_item.qty, 100)
 		self.assertEqual(stock_entry_item.putaway_rule, rule_1.name)
 
+		self.assertUnchangedItemsOnResave(stock_entry)
+
 		stock_entry.delete()
 		rule_1.delete()
 		rule_2.delete()
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_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index 2f37778..c38dfaa 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -8,7 +8,6 @@
  "engine": "InnoDB",
  "field_order": [
   "items_section",
-  "title",
   "naming_series",
   "stock_entry_type",
   "outgoing_stock_entry",
@@ -84,14 +83,6 @@
    "oldfieldtype": "Section Break"
   },
   {
-   "fieldname": "title",
-   "fieldtype": "Data",
-   "hidden": 1,
-   "label": "Title",
-   "no_copy": 1,
-   "print_hide": 1
-  },
-  {
    "fieldname": "naming_series",
    "fieldtype": "Select",
    "label": "Series",
@@ -353,9 +344,9 @@
   },
   {
    "fieldname": "scan_barcode",
-   "options": "Barcode",
    "fieldtype": "Data",
-   "label": "Scan Barcode"
+   "label": "Scan Barcode",
+   "options": "Barcode"
   },
   {
    "allow_bulk_edit": 1,
@@ -628,10 +619,11 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-08-20 19:19:31.514846",
+ "modified": "2022-02-07 12:55:14.614077",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Entry",
+ "naming_rule": "By \"Naming Series\" field",
  "owner": "Administrator",
  "permissions": [
   {
@@ -698,6 +690,7 @@
  "show_name_in_global_search": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
- "title_field": "title",
+ "states": [],
+ "title_field": "stock_entry_type",
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index c51c9bc..9ba007a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -76,7 +76,6 @@
 
 		self.validate_posting_time()
 		self.validate_purpose()
-		self.set_title()
 		self.validate_item()
 		self.validate_customer_provided_item()
 		self.validate_qty()
@@ -434,9 +433,10 @@
 				)
 
 	def set_actual_qty(self):
-		allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock"))
+		from erpnext.stock.stock_ledger import is_negative_stock_allowed
 
 		for d in self.get('items'):
+			allow_negative_stock = is_negative_stock_allowed(item_code=d.item_code)
 			previous_sle = get_previous_sle({
 				"item_code": d.item_code,
 				"warehouse": d.s_warehouse or d.t_warehouse,
@@ -1116,7 +1116,7 @@
 		self.set_actual_qty()
 		self.update_items_for_process_loss()
 		self.validate_customer_provided_item()
-		self.calculate_rate_and_amount()
+		self.calculate_rate_and_amount(raise_error_if_no_rate=False)
 
 	def set_scrap_items(self):
 		if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
@@ -1835,14 +1835,6 @@
 
 		return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
 
-	def set_title(self):
-		if frappe.flags.in_import and self.title:
-			# Allow updating title during data import/update
-			return
-
-		self.title = self.purpose
-
-
 @frappe.whitelist()
 def move_sample_to_retention_warehouse(company, items):
 	if isinstance(items, str):
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/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 33d9a6c..ec7fb0f 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -5,35 +5,41 @@
  "doctype": "DocType",
  "engine": "InnoDB",
  "field_order": [
+  "defaults_tab",
   "item_defaults_section",
   "item_naming_by",
   "item_group",
   "stock_uom",
-  "default_warehouse",
   "column_break_4",
-  "valuation_method",
+  "default_warehouse",
   "sample_retention_warehouse",
-  "use_naming_series",
-  "naming_series_prefix",
+  "valuation_method",
+  "price_list_defaults_section",
+  "auto_insert_price_list_rate_if_missing",
+  "column_break_12",
+  "update_existing_price_list_rate",
+  "stock_validations_tab",
   "section_break_9",
   "over_delivery_receipt_allowance",
-  "role_allowed_to_over_deliver_receive",
   "mr_qty_allowance",
-  "column_break_12",
-  "auto_insert_price_list_rate_if_missing",
-  "update_existing_price_list_rate",
+  "column_break_121",
+  "role_allowed_to_over_deliver_receive",
   "allow_negative_stock",
   "show_barcode_field",
   "clean_description_html",
   "quality_inspection_settings_section",
   "action_if_quality_inspection_is_not_submitted",
-  "column_break_21",
+  "column_break_23",
   "action_if_quality_inspection_is_rejected",
+  "serial_and_batch_item_settings_tab",
   "section_break_7",
   "automatically_set_serial_nos_based_on_fifo",
   "set_qty_in_transactions_based_on_serial_no_input",
   "column_break_10",
   "disable_serial_no_and_batch_selector",
+  "use_naming_series",
+  "naming_series_prefix",
+  "stock_planning_tab",
   "auto_material_request",
   "auto_indent",
   "column_break_27",
@@ -42,6 +48,7 @@
   "allow_from_dn",
   "column_break_31",
   "allow_from_pr",
+  "stock_closing_tab",
   "control_historical_stock_transactions_section",
   "stock_frozen_upto",
   "stock_frozen_upto_days",
@@ -92,7 +99,7 @@
    "fieldname": "valuation_method",
    "fieldtype": "Select",
    "label": "Default Valuation Method",
-   "options": "FIFO\nMoving Average"
+   "options": "FIFO\nMoving Average\nLIFO"
   },
   {
    "description": "The percentage you are allowed to receive or deliver more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed to receive 110 units.",
@@ -122,7 +129,7 @@
   {
    "fieldname": "section_break_7",
    "fieldtype": "Section Break",
-   "label": "Serialised and Batch Setting"
+   "label": "Serial & Batch Item Settings"
   },
   {
    "default": "0",
@@ -276,10 +283,6 @@
    "label": "Quality Inspection Settings"
   },
   {
-   "fieldname": "column_break_21",
-   "fieldtype": "Column Break"
-  },
-  {
    "default": "Stop",
    "fieldname": "action_if_quality_inspection_is_rejected",
    "fieldtype": "Select",
@@ -298,6 +301,44 @@
    "fieldname": "update_existing_price_list_rate",
    "fieldtype": "Check",
    "label": "Update Existing Price List Rate"
+  },
+  {
+   "fieldname": "defaults_tab",
+   "fieldtype": "Tab Break",
+   "label": "Defaults"
+  },
+  {
+   "fieldname": "stock_validations_tab",
+   "fieldtype": "Tab Break",
+   "label": "Stock Validations"
+  },
+  {
+   "fieldname": "stock_planning_tab",
+   "fieldtype": "Tab Break",
+   "label": "Stock Planning"
+  },
+  {
+   "fieldname": "stock_closing_tab",
+   "fieldtype": "Tab Break",
+   "label": "Stock Closing"
+  },
+  {
+   "fieldname": "serial_and_batch_item_settings_tab",
+   "fieldtype": "Tab Break",
+   "label": "Serial & Batch Item"
+  },
+  {
+   "fieldname": "column_break_23",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "price_list_defaults_section",
+   "fieldtype": "Section Break",
+   "label": "Price List Defaults"
+  },
+  {
+   "fieldname": "column_break_121",
+   "fieldtype": "Column Break"
   }
  ],
  "icon": "icon-cog",
@@ -305,7 +346,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-11-06 19:40:02.183592",
+ "modified": "2022-02-05 15:33:43.692736",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Settings",
@@ -324,5 +365,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "ASC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 06f8fa7..9bec5f7 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -6,6 +6,7 @@
 
 import frappe
 from frappe import _, throw
+from frappe.model import child_table_fields, default_fields
 from frappe.model.meta import get_field_precision
 from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
 
@@ -119,8 +120,15 @@
 		out.rate = args.rate or out.price_list_rate
 		out.amount = flt(args.qty) * flt(out.rate)
 
+	out = remove_standard_fields(out)
 	return out
 
+def remove_standard_fields(details):
+	for key in child_table_fields + default_fields:
+		details.pop(key, None)
+	return details
+
+
 def update_stock(args, out):
 	if (args.get("doctype") == "Delivery Note" or
 		(args.get("doctype") == "Sales Invoice" and args.get('update_stock'))) \
@@ -343,6 +351,7 @@
 
 	args.conversion_factor = out.conversion_factor
 	out.stock_qty = out.qty * out.conversion_factor
+	args.stock_qty = out.stock_qty
 
 	# calculate last purchase rate
 	if args.get('doctype') in purchase_doctypes:
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index e6dfc97..a89a403 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -252,6 +252,7 @@
 			key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
 
 			if d.voucher_type == "Stock Reconciliation":
+				# get difference in qty shift as actual qty
 				prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0)
 				d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty)
 
@@ -264,12 +265,16 @@
 
 			self.__update_balances(d, key)
 
+		if not self.filters.get("show_warehouse_wise_stock"):
+			# (Item 1, WH 1), (Item 1, WH 2) => (Item 1)
+			self.item_details = self.__aggregate_details_by_item(self.item_details)
+
 		return self.item_details
 
 	def __init_key_stores(self, row: Dict) -> Tuple:
 		"Initialise keys and FIFO Queue."
 
-		key = (row.name, row.warehouse) if self.filters.get('show_warehouse_wise_stock') else row.name
+		key = (row.name, row.warehouse)
 		self.item_details.setdefault(key, {"details": row, "fifo_queue": []})
 		fifo_queue = self.item_details[key]["fifo_queue"]
 
@@ -338,6 +343,27 @@
 
 		self.item_details[key]["has_serial_no"] = row.has_serial_no
 
+	def __aggregate_details_by_item(self, wh_wise_data: Dict) -> Dict:
+		"Aggregate Item-Wh wise data into single Item entry."
+		item_aggregated_data = {}
+		for key,row in wh_wise_data.items():
+			item = key[0]
+			if not item_aggregated_data.get(item):
+				item_aggregated_data.setdefault(item, {
+					"details": frappe._dict(),
+					"fifo_queue": [],
+					"qty_after_transaction": 0.0,
+					"total_qty": 0.0
+				})
+			item_row = item_aggregated_data.get(item)
+			item_row["details"].update(row["details"])
+			item_row["fifo_queue"].extend(row["fifo_queue"])
+			item_row["qty_after_transaction"] += flt(row["qty_after_transaction"])
+			item_row["total_qty"] += flt(row["total_qty"])
+			item_row["has_serial_no"] = row["has_serial_no"]
+
+		return item_aggregated_data
+
 	def __get_stock_ledger_entries(self) -> List[Dict]:
 		sle = frappe.qb.DocType("Stock Ledger Entry")
 		item = self.__get_item_query() # used as derived table in sle query
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
index 5ffe97f..9e9bed4 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
+++ b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
@@ -15,6 +15,7 @@
 50 qty is (today-the 1st) days old
 20 qty is (today-the 2nd) days old
 
+> Note: We generate FIFO slots warehouse wise as stock reconciliations from different warehouses can cause incorrect values.
 ### Calculation of FIFO Slots
 
 #### Case 1: Outward from sufficient balance qty
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
index 949bb7c..66d2f6b 100644
--- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -15,11 +15,12 @@
 		)
 
 	def test_normal_inward_outward_queue(self):
-		"Reference: Case 1 in stock_ageing_fifo_logic.md"
+		"Reference: Case 1 in stock_ageing_fifo_logic.md (same wh)"
 		sle = [
 			frappe._dict(
 				name="Flask Item",
 				actual_qty=30, qty_after_transaction=30,
+				warehouse="WH 1",
 				posting_date="2021-12-01", voucher_type="Stock Entry",
 				voucher_no="001",
 				has_serial_no=False, serial_no=None
@@ -27,6 +28,7 @@
 			frappe._dict(
 				name="Flask Item",
 				actual_qty=20, qty_after_transaction=50,
+				warehouse="WH 1",
 				posting_date="2021-12-02", voucher_type="Stock Entry",
 				voucher_no="002",
 				has_serial_no=False, serial_no=None
@@ -34,6 +36,7 @@
 			frappe._dict(
 				name="Flask Item",
 				actual_qty=(-10), qty_after_transaction=40,
+				warehouse="WH 1",
 				posting_date="2021-12-03", voucher_type="Stock Entry",
 				voucher_no="003",
 				has_serial_no=False, serial_no=None
@@ -50,11 +53,12 @@
 		self.assertEqual(queue[0][0], 20.0)
 
 	def test_insufficient_balance(self):
-		"Reference: Case 3 in stock_ageing_fifo_logic.md"
+		"Reference: Case 3 in stock_ageing_fifo_logic.md (same wh)"
 		sle = [
 			frappe._dict(
 				name="Flask Item",
 				actual_qty=(-30), qty_after_transaction=(-30),
+				warehouse="WH 1",
 				posting_date="2021-12-01", voucher_type="Stock Entry",
 				voucher_no="001",
 				has_serial_no=False, serial_no=None
@@ -62,6 +66,7 @@
 			frappe._dict(
 				name="Flask Item",
 				actual_qty=20, qty_after_transaction=(-10),
+				warehouse="WH 1",
 				posting_date="2021-12-02", voucher_type="Stock Entry",
 				voucher_no="002",
 				has_serial_no=False, serial_no=None
@@ -69,6 +74,7 @@
 			frappe._dict(
 				name="Flask Item",
 				actual_qty=20, qty_after_transaction=10,
+				warehouse="WH 1",
 				posting_date="2021-12-03", voucher_type="Stock Entry",
 				voucher_no="003",
 				has_serial_no=False, serial_no=None
@@ -76,6 +82,7 @@
 			frappe._dict(
 				name="Flask Item",
 				actual_qty=10, qty_after_transaction=20,
+				warehouse="WH 1",
 				posting_date="2021-12-03", voucher_type="Stock Entry",
 				voucher_no="004",
 				has_serial_no=False, serial_no=None
@@ -91,11 +98,16 @@
 		self.assertEqual(queue[0][0], 10.0)
 		self.assertEqual(queue[1][0], 10.0)
 
-	def test_stock_reconciliation(self):
+	def test_basic_stock_reconciliation(self):
+		"""
+		Ledger (same wh): [+30, reco reset >> 50, -10]
+		Bal: 40
+		"""
 		sle = [
 			frappe._dict(
 				name="Flask Item",
 				actual_qty=30, qty_after_transaction=30,
+				warehouse="WH 1",
 				posting_date="2021-12-01", voucher_type="Stock Entry",
 				voucher_no="001",
 				has_serial_no=False, serial_no=None
@@ -103,6 +115,7 @@
 			frappe._dict(
 				name="Flask Item",
 				actual_qty=0, qty_after_transaction=50,
+				warehouse="WH 1",
 				posting_date="2021-12-02", voucher_type="Stock Reconciliation",
 				voucher_no="002",
 				has_serial_no=False, serial_no=None
@@ -110,6 +123,7 @@
 			frappe._dict(
 				name="Flask Item",
 				actual_qty=(-10), qty_after_transaction=40,
+				warehouse="WH 1",
 				posting_date="2021-12-03", voucher_type="Stock Entry",
 				voucher_no="003",
 				has_serial_no=False, serial_no=None
@@ -122,5 +136,112 @@
 		queue = result["fifo_queue"]
 
 		self.assertEqual(result["qty_after_transaction"], result["total_qty"])
+		self.assertEqual(result["total_qty"], 40.0)
 		self.assertEqual(queue[0][0], 20.0)
 		self.assertEqual(queue[1][0], 20.0)
+
+	def test_sequential_stock_reco_same_warehouse(self):
+		"""
+		Test back to back stock recos (same warehouse).
+		Ledger: [reco opening >> +1000, reco reset >> 400, -10]
+		Bal: 390
+		"""
+		sle = [
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=0, qty_after_transaction=1000,
+				warehouse="WH 1",
+				posting_date="2021-12-01", voucher_type="Stock Reconciliation",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=0, qty_after_transaction=400,
+				warehouse="WH 1",
+				posting_date="2021-12-02", voucher_type="Stock Reconciliation",
+				voucher_no="003",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=(-10), qty_after_transaction=390,
+				warehouse="WH 1",
+				posting_date="2021-12-03", voucher_type="Stock Entry",
+				voucher_no="003",
+				has_serial_no=False, serial_no=None
+			)
+		]
+		slots = FIFOSlots(self.filters, sle).generate()
+
+		result = slots["Flask Item"]
+		queue = result["fifo_queue"]
+
+		self.assertEqual(result["qty_after_transaction"], result["total_qty"])
+		self.assertEqual(result["total_qty"], 390.0)
+		self.assertEqual(queue[0][0], 390.0)
+
+	def test_sequential_stock_reco_different_warehouse(self):
+		"""
+		Ledger:
+		WH	| Voucher | Qty
+		-------------------
+		WH1 | Reco	  | 1000
+		WH2 | Reco	  | 400
+		WH1 | SE	  | -10
+
+		Bal: WH1 bal + WH2 bal = 990 + 400 = 1390
+		"""
+		sle = [
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=0, qty_after_transaction=1000,
+				warehouse="WH 1",
+				posting_date="2021-12-01", voucher_type="Stock Reconciliation",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=0, qty_after_transaction=400,
+				warehouse="WH 2",
+				posting_date="2021-12-02", voucher_type="Stock Reconciliation",
+				voucher_no="003",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=(-10), qty_after_transaction=990,
+				warehouse="WH 1",
+				posting_date="2021-12-03", voucher_type="Stock Entry",
+				voucher_no="004",
+				has_serial_no=False, serial_no=None
+			)
+		]
+
+		item_wise_slots, item_wh_wise_slots = generate_item_and_item_wh_wise_slots(
+			filters=self.filters,sle=sle
+		)
+
+		# test without 'show_warehouse_wise_stock'
+		item_result = item_wise_slots["Flask Item"]
+		queue = item_result["fifo_queue"]
+
+		self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
+		self.assertEqual(item_result["total_qty"], 1390.0)
+		self.assertEqual(queue[0][0], 990.0)
+		self.assertEqual(queue[1][0], 400.0)
+
+		# test with 'show_warehouse_wise_stock' checked
+		item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots]
+		self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"])
+
+def generate_item_and_item_wh_wise_slots(filters, sle):
+	"Return results with and without 'show_warehouse_wise_stock'"
+	item_wise_slots = FIFOSlots(filters, sle).generate()
+
+	filters.show_warehouse_wise_stock = True
+	item_wh_wise_slots = FIFOSlots(filters, sle).generate()
+	filters.show_warehouse_wise_stock = False
+
+	return item_wise_slots, item_wh_wise_slots
\ No newline at end of file
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
index 48753b0..cb35bf7 100644
--- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -167,7 +167,7 @@
 		{
 			"fieldname": "stock_queue",
 			"fieldtype": "Data",
-			"label": "FIFO Queue",
+			"label": "FIFO/LIFO Queue",
 		},
 
 		{
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index 6663458..62017e4 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -3,10 +3,9 @@
 
 
 import frappe
-from frappe.utils import cstr, flt, nowdate, nowtime
+from frappe.utils import cstr, flt, now, nowdate, nowtime
 
 from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
-from erpnext.stock.utils import update_bin
 
 
 def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, only_bin=False):
@@ -175,6 +174,7 @@
 			bin.set(field, flt(value))
 			mismatch = True
 
+	bin.modified = now()
 	if mismatch:
 		bin.set_projected_qty()
 		bin.db_update()
@@ -227,8 +227,6 @@
 			"sle_id": sle_doc.name
 		})
 
-		update_bin(args)
-
 		create_repost_item_valuation_entry({
 			"item_code": d[0],
 			"warehouse": d[1],
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 0a7ab40..00ca81f 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -3,6 +3,7 @@
 
 import copy
 import json
+from typing import Optional
 
 import frappe
 from frappe import _
@@ -16,7 +17,7 @@
 	get_or_make_bin,
 	get_valuation_method,
 )
-from erpnext.stock.valuation import FIFOValuation
+from erpnext.stock.valuation import FIFOValuation, LIFOValuation
 
 
 class NegativeStockError(frappe.ValidationError): pass
@@ -268,11 +269,10 @@
 		self.verbose = verbose
 		self.allow_zero_rate = allow_zero_rate
 		self.via_landed_cost_voucher = via_landed_cost_voucher
-		self.allow_negative_stock = allow_negative_stock \
-			or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
+		self.item_code = args.get("item_code")
+		self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(item_code=self.item_code)
 
 		self.args = frappe._dict(args)
-		self.item_code = args.get("item_code")
 		if self.args.sle_id:
 			self.args['name'] = self.args.sle_id
 
@@ -461,7 +461,7 @@
 					self.wh_data.qty_after_transaction += flt(sle.actual_qty)
 					self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
 				else:
-					self.update_fifo_values(sle)
+					self.update_queue_values(sle)
 					self.wh_data.qty_after_transaction += flt(sle.actual_qty)
 
 		# rounding as per precision
@@ -701,14 +701,18 @@
 						sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
 						currency=erpnext.get_company_currency(sle.company), company=sle.company)
 
-	def update_fifo_values(self, sle):
+	def update_queue_values(self, sle):
 		incoming_rate = flt(sle.incoming_rate)
 		actual_qty = flt(sle.actual_qty)
 		outgoing_rate = flt(sle.outgoing_rate)
 
-		fifo_queue = FIFOValuation(self.wh_data.stock_queue)
+		if self.valuation_method == "LIFO":
+			stock_queue = LIFOValuation(self.wh_data.stock_queue)
+		else:
+			stock_queue = FIFOValuation(self.wh_data.stock_queue)
+
 		if actual_qty > 0:
-			fifo_queue.add_stock(qty=actual_qty, rate=incoming_rate)
+			stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
 		else:
 			def rate_generator() -> float:
 				allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
@@ -719,11 +723,11 @@
 				else:
 					return 0.0
 
-			fifo_queue.remove_stock(qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator)
+			stock_queue.remove_stock(qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator)
 
-		stock_qty, stock_value = fifo_queue.get_total_stock_and_value()
+		stock_qty, stock_value = stock_queue.get_total_stock_and_value()
 
-		self.wh_data.stock_queue = fifo_queue.get_state()
+		self.wh_data.stock_queue = stock_queue.state
 		self.wh_data.stock_value = stock_value
 		if stock_qty:
 			self.wh_data.valuation_rate = stock_value / stock_qty
@@ -1045,10 +1049,7 @@
 		)"""
 
 def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
-	allow_negative_stock = cint(allow_negative_stock) \
-		or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
-
-	if allow_negative_stock:
+	if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
 		return
 	if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
 		return
@@ -1117,3 +1118,11 @@
 			and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
 		limit 1
 	""", args, as_dict=1)
+
+
+def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
+	if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
+		return True
+	if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
+		return True
+	return False
diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py
index 85788ba..648d440 100644
--- a/erpnext/stock/tests/test_valuation.py
+++ b/erpnext/stock/tests/test_valuation.py
@@ -1,16 +1,21 @@
+import json
 import unittest
 
+import frappe
 from hypothesis import given
 from hypothesis import strategies as st
 
-from erpnext.stock.valuation import FIFOValuation, _round_off_if_near_zero
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.stock.valuation import FIFOValuation, LIFOValuation, _round_off_if_near_zero
+from erpnext.tests.utils import ERPNextTestCase
 
 qty_gen = st.floats(min_value=-1e6, max_value=1e6)
 value_gen = st.floats(min_value=1, max_value=1e6)
 stock_queue_generator = st.lists(st.tuples(qty_gen, value_gen), min_size=10)
 
 
-class TestFifoValuation(unittest.TestCase):
+class TestFIFOValuation(unittest.TestCase):
 
 	def setUp(self):
 		self.queue = FIFOValuation([])
@@ -164,3 +169,184 @@
 				total_value -= sum(q * r for q, r in consumed)
 			self.assertTotalQty(total_qty)
 			self.assertTotalValue(total_value)
+
+
+class TestLIFOValuation(unittest.TestCase):
+
+	def setUp(self):
+		self.stack = LIFOValuation([])
+
+	def tearDown(self):
+		qty, value = self.stack.get_total_stock_and_value()
+		self.assertTotalQty(qty)
+		self.assertTotalValue(value)
+
+	def assertTotalQty(self, qty):
+		self.assertAlmostEqual(sum(q for q, _ in self.stack), qty, msg=f"stack: {self.stack}", places=4)
+
+	def assertTotalValue(self, value):
+		self.assertAlmostEqual(sum(q * r for q, r in self.stack), value, msg=f"stack: {self.stack}", places=2)
+
+	def test_simple_addition(self):
+		self.stack.add_stock(1, 10)
+		self.assertTotalQty(1)
+
+	def test_merge_new_stock(self):
+		self.stack.add_stock(1, 10)
+		self.stack.add_stock(1, 10)
+		self.assertEqual(self.stack, [[2, 10]])
+
+	def test_simple_removal(self):
+		self.stack.add_stock(1, 10)
+		self.stack.remove_stock(1)
+		self.assertTotalQty(0)
+
+	def test_adding_negative_stock_keeps_rate(self):
+		self.stack = LIFOValuation([[-5.0, 100]])
+		self.stack.add_stock(1, 10)
+		self.assertEqual(self.stack, [[-4, 100]])
+
+	def test_adding_negative_stock_updates_rate(self):
+		self.stack = LIFOValuation([[-5.0, 100]])
+		self.stack.add_stock(6, 10)
+		self.assertEqual(self.stack, [[1, 10]])
+
+	def test_rounding_off(self):
+		self.stack.add_stock(1.0, 1.0)
+		self.stack.remove_stock(1.0 - 1e-9)
+		self.assertTotalQty(0)
+
+	def test_lifo_consumption(self):
+		self.stack.add_stock(10, 10)
+		self.stack.add_stock(10, 20)
+		consumed = self.stack.remove_stock(15)
+		self.assertEqual(consumed, [[10, 20], [5, 10]])
+		self.assertTotalQty(5)
+
+	def test_lifo_consumption_going_negative(self):
+		self.stack.add_stock(10, 10)
+		self.stack.add_stock(10, 20)
+		consumed = self.stack.remove_stock(25)
+		self.assertEqual(consumed, [[10, 20], [10, 10], [5, 10]])
+		self.assertTotalQty(-5)
+
+	def test_lifo_consumption_multiple(self):
+		self.stack.add_stock(1, 1)
+		self.stack.add_stock(2, 2)
+		consumed = self.stack.remove_stock(1)
+		self.assertEqual(consumed, [[1, 2]])
+
+		self.stack.add_stock(3, 3)
+		consumed = self.stack.remove_stock(4)
+		self.assertEqual(consumed, [[3, 3], [1, 2]])
+
+		self.stack.add_stock(4, 4)
+		consumed = self.stack.remove_stock(5)
+		self.assertEqual(consumed, [[4, 4], [1, 1]])
+
+		self.stack.add_stock(5, 5)
+		consumed = self.stack.remove_stock(5)
+		self.assertEqual(consumed, [[5, 5]])
+
+
+	@given(stock_queue_generator)
+	def test_lifo_qty_hypothesis(self, stock_stack):
+		self.stack = LIFOValuation([])
+		total_qty = 0
+
+		for qty, rate in stock_stack:
+			if qty == 0:
+				continue
+			if qty > 0:
+				self.stack.add_stock(qty, rate)
+				total_qty += qty
+			else:
+				qty = abs(qty)
+				consumed = self.stack.remove_stock(qty)
+				self.assertAlmostEqual(qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}")
+				total_qty -= qty
+			self.assertTotalQty(total_qty)
+
+	@given(stock_queue_generator)
+	def test_lifo_qty_value_nonneg_hypothesis(self, stock_stack):
+		self.stack = LIFOValuation([])
+		total_qty = 0.0
+		total_value = 0.0
+
+		for qty, rate in stock_stack:
+			# don't allow negative stock
+			if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
+				continue
+			if qty > 0:
+				self.stack.add_stock(qty, rate)
+				total_qty += qty
+				total_value += qty * rate
+			else:
+				qty = abs(qty)
+				consumed = self.stack.remove_stock(qty)
+				self.assertAlmostEqual(qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}")
+				total_qty -= qty
+				total_value -= sum(q * r for q, r in consumed)
+			self.assertTotalQty(total_qty)
+			self.assertTotalValue(total_value)
+
+class TestLIFOValuationSLE(ERPNextTestCase):
+	ITEM_CODE = "_Test LIFO item"
+	WAREHOUSE = "_Test Warehouse - _TC"
+
+	@classmethod
+	def setUpClass(cls) -> None:
+		super().setUpClass()
+		make_item(cls.ITEM_CODE, {"valuation_method": "LIFO"})
+
+	def _make_stock_entry(self, qty, rate=None):
+		kwargs = {
+			"item_code": self.ITEM_CODE,
+			"from_warehouse" if qty < 0 else "to_warehouse": self.WAREHOUSE,
+			"rate": rate,
+			"qty": abs(qty),
+		}
+		return make_stock_entry(**kwargs)
+
+	def assertStockQueue(self, se, expected_queue):
+		sle_name = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": se.name, "is_cancelled": 0, "voucher_type": "Stock Entry"})
+		sle = frappe.get_doc("Stock Ledger Entry", sle_name)
+
+		stock_queue = json.loads(sle.stock_queue)
+
+		total_qty, total_value = LIFOValuation(stock_queue).get_total_stock_and_value()
+		self.assertEqual(sle.qty_after_transaction, total_qty)
+		self.assertEqual(sle.stock_value, total_value)
+
+		if total_qty > 0:
+			self.assertEqual(stock_queue, expected_queue)
+
+
+	def test_lifo_values(self):
+
+		in1 = self._make_stock_entry(1, 1)
+		self.assertStockQueue(in1, [[1, 1]])
+
+		in2 = self._make_stock_entry(2, 2)
+		self.assertStockQueue(in2, [[1, 1], [2, 2]])
+
+		out1 = self._make_stock_entry(-1)
+		self.assertStockQueue(out1, [[1, 1], [1, 2]])
+
+		in3 = self._make_stock_entry(3, 3)
+		self.assertStockQueue(in3, [[1, 1], [1, 2], [3, 3]])
+
+		out2 = self._make_stock_entry(-4)
+		self.assertStockQueue(out2, [[1, 1]])
+
+		in4 = self._make_stock_entry(4, 4)
+		self.assertStockQueue(in4, [[1, 1], [4,4]])
+
+		out3 = self._make_stock_entry(-5)
+		self.assertStockQueue(out3, [])
+
+		in5 = self._make_stock_entry(5, 5)
+		self.assertStockQueue(in5, [[5, 5]])
+
+		out5 = self._make_stock_entry(-5)
+		self.assertStockQueue(out5, [])
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 7c63c17..7263e39 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -9,6 +9,7 @@
 from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
 
 import erpnext
+from erpnext.stock.valuation import FIFOValuation, LIFOValuation
 
 
 class InvalidWarehouseCompany(frappe.ValidationError): pass
@@ -205,16 +206,6 @@
 
 	return bin_obj
 
-def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
-	"""WARNING: This function is deprecated. Inline this function instead of using it."""
-	from erpnext.stock.doctype.bin.bin import update_stock
-	is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
-	if is_stock_item:
-		bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
-		update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher)
-	else:
-		frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
-
 @frappe.whitelist()
 def get_incoming_rate(args, raise_error_if_no_rate=True):
 	"""Get Incoming Rate based on valuation method"""
@@ -228,10 +219,10 @@
 	else:
 		valuation_method = get_valuation_method(args.get("item_code"))
 		previous_sle = get_previous_sle(args)
-		if valuation_method == 'FIFO':
+		if valuation_method in ('FIFO', 'LIFO'):
 			if previous_sle:
 				previous_stock_queue = json.loads(previous_sle.get('stock_queue', '[]') or '[]')
-				in_rate = get_fifo_rate(previous_stock_queue, args.get("qty") or 0) if previous_stock_queue else 0
+				in_rate = _get_fifo_lifo_rate(previous_stock_queue, args.get("qty") or 0, valuation_method) if previous_stock_queue else 0
 		elif valuation_method == 'Moving Average':
 			in_rate = previous_sle.get('valuation_rate') or 0
 
@@ -261,29 +252,25 @@
 
 def get_fifo_rate(previous_stock_queue, qty):
 	"""get FIFO (average) Rate from Queue"""
-	if flt(qty) >= 0:
-		total = sum(f[0] for f in previous_stock_queue)
-		return sum(flt(f[0]) * flt(f[1]) for f in previous_stock_queue) / flt(total) if total else 0.0
-	else:
-		available_qty_for_outgoing, outgoing_cost = 0, 0
-		qty_to_pop = abs(flt(qty))
-		while qty_to_pop and previous_stock_queue:
-			batch = previous_stock_queue[0]
-			if 0 < batch[0] <= qty_to_pop:
-				# if batch qty > 0
-				# not enough or exactly same qty in current batch, clear batch
-				available_qty_for_outgoing += flt(batch[0])
-				outgoing_cost += flt(batch[0]) * flt(batch[1])
-				qty_to_pop -= batch[0]
-				previous_stock_queue.pop(0)
-			else:
-				# all from current batch
-				available_qty_for_outgoing += flt(qty_to_pop)
-				outgoing_cost += flt(qty_to_pop) * flt(batch[1])
-				batch[0] -= qty_to_pop
-				qty_to_pop = 0
+	return _get_fifo_lifo_rate(previous_stock_queue, qty, "FIFO")
 
-		return outgoing_cost / available_qty_for_outgoing
+def get_lifo_rate(previous_stock_queue, qty):
+	"""get LIFO (average) Rate from Queue"""
+	return _get_fifo_lifo_rate(previous_stock_queue, qty, "LIFO")
+
+
+def _get_fifo_lifo_rate(previous_stock_queue, qty, method):
+	ValuationKlass = LIFOValuation if method == "LIFO" else FIFOValuation
+
+	stock_queue = ValuationKlass(previous_stock_queue)
+	if flt(qty) >= 0:
+		total_qty, total_value = stock_queue.get_total_stock_and_value()
+		return total_value / total_qty if total_qty else 0.0
+	else:
+		popped_bins = stock_queue.remove_stock(abs(flt(qty)))
+
+		total_qty, total_value = ValuationKlass(popped_bins).get_total_stock_and_value()
+		return total_value / total_qty if total_qty else 0.0
 
 def get_valid_serial_nos(sr_nos, qty=0, item_code=''):
 	"""split serial nos, validate and return list of valid serial nos"""
diff --git a/erpnext/stock/valuation.py b/erpnext/stock/valuation.py
index 45c5083..ee9477e 100644
--- a/erpnext/stock/valuation.py
+++ b/erpnext/stock/valuation.py
@@ -1,15 +1,54 @@
+from abc import ABC, abstractmethod, abstractproperty
 from typing import Callable, List, NewType, Optional, Tuple
 
 from frappe.utils import flt
 
-FifoBin = NewType("FifoBin", List[float])
+StockBin = NewType("StockBin", List[float])  # [[qty, rate], ...]
 
 # Indexes of values inside FIFO bin 2-tuple
 QTY = 0
 RATE = 1
 
 
-class FIFOValuation:
+class BinWiseValuation(ABC):
+
+	@abstractmethod
+	def add_stock(self, qty: float, rate: float) -> None:
+		pass
+
+	@abstractmethod
+	def remove_stock(
+		self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] = None
+	) -> List[StockBin]:
+		pass
+
+	@abstractproperty
+	def state(self) -> List[StockBin]:
+		pass
+
+	def get_total_stock_and_value(self) -> Tuple[float, float]:
+		total_qty = 0.0
+		total_value = 0.0
+
+		for qty, rate in self.state:
+			total_qty += flt(qty)
+			total_value += flt(qty) * flt(rate)
+
+		return _round_off_if_near_zero(total_qty), _round_off_if_near_zero(total_value)
+
+	def __repr__(self):
+		return str(self.state)
+
+	def __iter__(self):
+		return iter(self.state)
+
+	def __eq__(self, other):
+		if isinstance(other, list):
+			return self.state == other
+		return type(self) == type(other) and self.state == other.state
+
+
+class FIFOValuation(BinWiseValuation):
 	"""Valuation method where a queue of all the incoming stock is maintained.
 
 	New stock is added at end of the queue.
@@ -24,34 +63,14 @@
 	# ref: https://docs.python.org/3/reference/datamodel.html#slots
 	__slots__ = ["queue",]
 
-	def __init__(self, state: Optional[List[FifoBin]]):
-		self.queue: List[FifoBin] = state if state is not None else []
+	def __init__(self, state: Optional[List[StockBin]]):
+		self.queue: List[StockBin] = state if state is not None else []
 
-	def __repr__(self):
-		return str(self.queue)
-
-	def __iter__(self):
-		return iter(self.queue)
-
-	def __eq__(self, other):
-		if isinstance(other, list):
-			return self.queue == other
-		return self.queue == other.queue
-
-	def get_state(self) -> List[FifoBin]:
+	@property
+	def state(self) -> List[StockBin]:
 		"""Get current state of queue."""
 		return self.queue
 
-	def get_total_stock_and_value(self) -> Tuple[float, float]:
-		total_qty = 0.0
-		total_value = 0.0
-
-		for qty, rate in self.queue:
-			total_qty += flt(qty)
-			total_value += flt(qty) * flt(rate)
-
-		return _round_off_if_near_zero(total_qty), _round_off_if_near_zero(total_value)
-
 	def add_stock(self, qty: float, rate: float) -> None:
 		"""Update fifo queue with new stock.
 
@@ -78,7 +97,7 @@
 
 	def remove_stock(
 		self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] = None
-	) -> List[FifoBin]:
+	) -> List[StockBin]:
 		"""Remove stock from the queue and return popped bins.
 
 		args:
@@ -136,6 +155,101 @@
 		return consumed_bins
 
 
+class LIFOValuation(BinWiseValuation):
+	"""Valuation method where a *stack* of all the incoming stock is maintained.
+
+	New stock is added at top of the stack.
+	Qty consumption happens on Last In First Out basis.
+
+	Stack is implemented using "bins" of [qty, rate].
+
+	ref: https://en.wikipedia.org/wiki/FIFO_and_LIFO_accounting
+	Implementation detail: appends and pops both at end of list.
+	"""
+
+	# specifying the attributes to save resources
+	# ref: https://docs.python.org/3/reference/datamodel.html#slots
+	__slots__ = ["stack",]
+
+	def __init__(self, state: Optional[List[StockBin]]):
+		self.stack: List[StockBin] = state if state is not None else []
+
+	@property
+	def state(self) -> List[StockBin]:
+		"""Get current state of stack."""
+		return self.stack
+
+	def add_stock(self, qty: float, rate: float) -> None:
+		"""Update lifo stack with new stock.
+
+			args:
+				qty: new quantity to add
+				rate: incoming rate of new quantity.
+
+			Behaviour of this is same as FIFO valuation.
+		"""
+		if not len(self.stack):
+			self.stack.append([0, 0])
+
+		# last row has the same rate, merge new bin.
+		if self.stack[-1][RATE] == rate:
+			self.stack[-1][QTY] += qty
+		else:
+			# Item has a positive balance qty, add new entry
+			if self.stack[-1][QTY] > 0:
+				self.stack.append([qty, rate])
+			else:  # negative balance qty
+				qty = self.stack[-1][QTY] + qty
+				if qty > 0:  # new balance qty is positive
+					self.stack[-1] = [qty, rate]
+				else:  # new balance qty is still negative, maintain same rate
+					self.stack[-1][QTY] = qty
+
+
+	def remove_stock(
+		self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] = None
+	) -> List[StockBin]:
+		"""Remove stock from the stack and return popped bins.
+
+		args:
+			qty: quantity to remove
+			rate: outgoing rate - ignored. Kept for backwards compatibility.
+			rate_generator: function to be called if stack is not found and rate is required.
+		"""
+		if not rate_generator:
+			rate_generator = lambda : 0.0  # noqa
+
+		consumed_bins = []
+		while qty:
+			if not len(self.stack):
+				# rely on rate generator.
+				self.stack.append([0, rate_generator()])
+
+			# start at the end.
+			index = -1
+
+			stock_bin = self.stack[index]
+			if qty >= stock_bin[QTY]:
+				# consume current bin
+				qty = _round_off_if_near_zero(qty - stock_bin[QTY])
+				to_consume = self.stack.pop(index)
+				consumed_bins.append(list(to_consume))
+
+				if not self.stack and qty:
+					# stock finished, qty still remains to be withdrawn
+					# negative stock, keep in as a negative bin
+					self.stack.append([-qty, outgoing_rate or stock_bin[RATE]])
+					consumed_bins.append([qty, outgoing_rate or stock_bin[RATE]])
+					break
+			else:
+				# qty found in current bin consume it and exit
+				stock_bin[QTY] = _round_off_if_near_zero(stock_bin[QTY] - qty)
+				consumed_bins.append([qty, stock_bin[RATE]])
+				qty = 0
+
+		return consumed_bins
+
+
 def _round_off_if_near_zero(number: float, precision: int = 7) -> float:
 	"""Rounds off the number to zero only if number is close to zero for decimal
 	specified in precision. Precision defaults to 7.
diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py
index df2dc8b..3299c88 100644
--- a/erpnext/tests/test_point_of_sale.py
+++ b/erpnext/tests/test_point_of_sale.py
@@ -1,15 +1,25 @@
 # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
 # MIT License. See license.txt
 
+import unittest
+
+import frappe
 
 from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
 from erpnext.selling.page.point_of_sale.point_of_sale import get_items
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestPointOfSale(ERPNextTestCase):
+class TestPointOfSale(unittest.TestCase):
+	@classmethod
+	def setUpClass(cls) -> None:
+		frappe.db.savepoint('before_test_point_of_sale')
+
+	@classmethod
+	def tearDownClass(cls) -> None:
+		frappe.db.rollback(save_point='before_test_point_of_sale')
+
 	def test_item_search(self):
 		"""
 		Test Stock and Service Item Search.
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/translations/de.csv b/erpnext/translations/de.csv
index 4a6c834..cf73564 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -913,6 +913,7 @@
 Email Address,E-Mail-Adresse,
 "Email Address must be unique, already exists for {0}","E-Mail-Adresse muss eindeutig sein, diese wird bereits für {0} verwendet",
 Email Digest: ,E-Mail-Bericht:,
+Email Digest Recipient,E-Mail-Berichtsempfänger,
 Email Reminders will be sent to all parties with email contacts,E-Mail-Erinnerungen werden an alle Parteien mit E-Mail-Kontakten gesendet,
 Email Sent,E-Mail wurde versandt,
 Email Template,E-Mail-Vorlage,
@@ -2944,7 +2945,7 @@
 Temporary Opening,Temporäre Eröffnungskonten,
 Terms and Conditions,Allgemeine Geschäftsbedingungen,
 Terms and Conditions Template,Vorlage für Allgemeine Geschäftsbedingungen,
-Territory,Region,
+Territory,Gebiet,
 Test,Test,
 Thank you,Danke,
 Thank you for your business!,Vielen Dank für Ihr Unternehmen!,
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
diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.js b/erpnext/utilities/doctype/rename_tool/rename_tool.js
index 7823055..5553e44 100644
--- a/erpnext/utilities/doctype/rename_tool/rename_tool.js
+++ b/erpnext/utilities/doctype/rename_tool/rename_tool.js
@@ -13,6 +13,12 @@
 	},
 	refresh: function(frm) {
 		frm.disable_save();
+
+		frm.get_field("file_to_rename").df.options = {
+			restrictions: {
+				allowed_file_types: [".csv"],
+			},
+		};
 		if (!frm.doc.file_to_rename) {
 			frm.get_field("rename_log").$wrapper.html("");
 		}