Merge pull request #31225 from vishdha/fix_date_range_print

fix: Print/PDF for financial statement reports displays either wrong date range or wrong fiscal year
diff --git a/.github/stale.yml b/.github/stale.yml
index cdce0ae..da15d32 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -23,3 +23,5 @@
     activity occurs, but it only takes a comment to keep a contribution alive
     :) Also, even if it is closed, you can always reopen the PR when you're
     ready. Thank you for contributing.
+
+only: pulls
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index e6da666..23ad223 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -545,7 +545,16 @@
 					from_repost=from_repost,
 				)
 			elif self.docstatus == 2:
+				provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
 				make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
+				if provisional_entries:
+					for entry in provisional_entries:
+						frappe.db.set_value(
+							"GL Entry",
+							{"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no},
+							"is_cancelled",
+							1,
+						)
 
 			if update_outstanding == "No":
 				update_outstanding_amt(
@@ -1127,7 +1136,7 @@
 		# Stock ledger value is not matching with the warehouse amount
 		if (
 			self.update_stock
-			and voucher_wise_stock_value.get(item.name)
+			and voucher_wise_stock_value.get((item.name, item.warehouse))
 			and warehouse_debit_amount
 			!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
 		):
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 30d26ac..3c70e24 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -27,12 +27,13 @@
 	make_purchase_receipt,
 )
 from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
+from erpnext.stock.tests.test_utils import StockTestMixin
 
 test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
 test_ignore = ["Serial No"]
 
 
-class TestPurchaseInvoice(unittest.TestCase):
+class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
 	@classmethod
 	def setUpClass(self):
 		unlink_payment_on_cancel_of_invoice()
@@ -693,6 +694,80 @@
 			self.assertEqual(expected_values[gle.account][0], gle.debit)
 			self.assertEqual(expected_values[gle.account][1], gle.credit)
 
+	def test_standalone_return_using_pi(self):
+		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+		item = self.make_item().name
+		company = "_Test Company with perpetual inventory"
+		warehouse = "Stores - TCP1"
+
+		make_stock_entry(item_code=item, target=warehouse, qty=50, rate=120)
+
+		return_pi = make_purchase_invoice(
+			is_return=1,
+			item=item,
+			qty=-10,
+			update_stock=1,
+			rate=100,
+			company=company,
+			warehouse=warehouse,
+			cost_center="Main - TCP1",
+		)
+
+		# assert that stock consumption is with actual rate
+		self.assertGLEs(
+			return_pi,
+			[{"credit": 1200, "debit": 0}],
+			gle_filters={"account": "Stock In Hand - TCP1"},
+		)
+
+		# assert loss booked in COGS
+		self.assertGLEs(
+			return_pi,
+			[{"credit": 0, "debit": 200}],
+			gle_filters={"account": "Cost of Goods Sold - TCP1"},
+		)
+
+	def test_return_with_lcv(self):
+		from erpnext.controllers.sales_and_purchase_return import make_return_doc
+		from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
+			create_landed_cost_voucher,
+		)
+
+		item = self.make_item().name
+		company = "_Test Company with perpetual inventory"
+		warehouse = "Stores - TCP1"
+		cost_center = "Main - TCP1"
+
+		pi = make_purchase_invoice(
+			item=item,
+			company=company,
+			warehouse=warehouse,
+			cost_center=cost_center,
+			update_stock=1,
+			qty=10,
+			rate=100,
+		)
+
+		# Create landed cost voucher - will increase valuation of received item by 10
+		create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company, charges=100)
+		return_pi = make_return_doc(pi.doctype, pi.name)
+		return_pi.save().submit()
+
+		# assert that stock consumption is with actual in rate
+		self.assertGLEs(
+			return_pi,
+			[{"credit": 1100, "debit": 0}],
+			gle_filters={"account": "Stock In Hand - TCP1"},
+		)
+
+		# assert loss booked in COGS
+		self.assertGLEs(
+			return_pi,
+			[{"credit": 0, "debit": 100}],
+			gle_filters={"account": "Cost of Goods Sold - TCP1"},
+		)
+
 	def test_multi_currency_gle(self):
 		pi = make_purchase_invoice(
 			supplier="_Test Supplier USD",
@@ -1526,6 +1601,18 @@
 
 		check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
 
+		# Cancel purchase invoice to check reverse provisional entry cancellation
+		pi.cancel()
+
+		expected_gle_for_purchase_receipt_post_pi_cancel = [
+			["Provision Account - _TC", 0, 250, pi.posting_date],
+			["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
+		]
+
+		check_gl_entries(
+			self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
+		)
+
 		company.enable_provisional_accounting_for_non_stock_items = 0
 		company.save()
 
diff --git a/erpnext/accounts/module_onboarding/accounts/accounts.json b/erpnext/accounts/module_onboarding/accounts/accounts.json
index aa7cdf7..b9040e3 100644
--- a/erpnext/accounts/module_onboarding/accounts/accounts.json
+++ b/erpnext/accounts/module_onboarding/accounts/accounts.json
@@ -13,7 +13,7 @@
  "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
  "idx": 0,
  "is_complete": 0,
- "modified": "2022-01-18 18:35:52.326688",
+ "modified": "2022-06-07 14:29:21.352132",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Accounts",
diff --git a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json
index 67553ba..0973ab3 100644
--- a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json
+++ b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json
@@ -1,8 +1,8 @@
 {
- "action": "Watch Video",
+ "action": "Go to Page",
  "action_label": "Learn more about Chart of Accounts",
  "callback_message": "You can continue with the onboarding after exploring this page",
- "callback_title": "Awesome Work",
+ "callback_title": "Explore Chart of Accounts",
  "creation": "2020-05-13 19:58:20.928127",
  "description": "# Chart Of Accounts\n\nERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to business and legal requirements.",
  "docstatus": 0,
@@ -12,7 +12,7 @@
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-08-13 11:46:25.878506",
+ "modified": "2022-06-07 14:21:26.264769",
  "modified_by": "Administrator",
  "name": "Chart of Accounts",
  "owner": "Administrator",
diff --git a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json
index 9f4c873..b6e9f5c 100644
--- a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json
+++ b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json
@@ -2,14 +2,14 @@
  "action": "Create Entry",
  "action_label": "Manage Sales Tax Templates",
  "creation": "2020-05-13 19:29:43.844463",
- "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n",
+ "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n\n[Checkout pre-configured taxes](/app/sales-taxes-and-charges-template)\n",
  "docstatus": 0,
  "doctype": "Onboarding Step",
  "idx": 0,
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-08-13 11:48:37.238610",
+ "modified": "2022-06-07 14:27:15.906286",
  "modified_by": "Administrator",
  "name": "Setup Taxes",
  "owner": "Administrator",
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
index f4fd06b..f2bf942 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
@@ -42,7 +42,7 @@
 
 	{% if(filters.show_future_payments) { %}
 		{% var balance_row = data.slice(-1).pop();
-			var start = filters.based_on_payment_terms ? 13 : 11;
+			var start = report.columns.findIndex((elem) => (elem.fieldname == 'age'));
 			var range1 = report.columns[start].label;
 			var range2 = report.columns[start+1].label;
 			var range3 = report.columns[start+2].label;
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index 777d96c..33bd3c7 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -367,8 +367,8 @@
 		if not filters.get(field) or field in accounting_dimensions_list:
 			return ""
 		return f""" and exists(select name from `tab{table}`
-			 where parent=`tabSales Invoice`.name
-			 	and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
+				where parent=`tabSales Invoice`.name
+					and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
 
 	conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
 	conditions += get_sales_invoice_item_field_condition("cost_center")
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index e5a4ed2..6bd08ad 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -160,14 +160,12 @@
 	if filters.project:
 		additional_conditions += " and project = %(project)s"
 
-	if filters.finance_book:
-		fb_conditions = " AND finance_book = %(finance_book)s"
-		if filters.include_default_book_entries:
-			fb_conditions = (
-				" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
-			)
-
-		additional_conditions += fb_conditions
+	if filters.get("include_default_book_entries"):
+		additional_conditions += (
+			" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
+		)
+	else:
+		additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
 
 	accounting_dimensions = get_accounting_dimensions(as_list=False)
 
diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py
index 19fe74f..3f06c30 100644
--- a/erpnext/accounts/test/test_reports.py
+++ b/erpnext/accounts/test/test_reports.py
@@ -28,6 +28,7 @@
 	("Item-wise Sales Register", {}),
 	("Item-wise Purchase Register", {}),
 	("Sales Register", {}),
+	("Sales Register", {"item_group": "All Item Groups"}),
 	("Purchase Register", {}),
 	(
 		"Tax Detail",
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index 97d0ba0..43152e8 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -84,6 +84,9 @@
 		self.save()
 
 	def validate_internal_supplier(self):
+		if not self.is_internal_supplier:
+			self.represents_company = ""
+
 		internal_supplier = frappe.db.get_value(
 			"Supplier",
 			{
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index bd4b59b..d24ac3f 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -316,7 +316,7 @@
 	return data[0]
 
 
-def make_return_doc(doctype, source_name, target_doc=None):
+def make_return_doc(doctype: str, source_name: str, target_doc=None):
 	from frappe.model.mapper import get_mapped_doc
 
 	from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py
index 61b4b9e..1f649c7 100644
--- a/erpnext/e_commerce/redisearch_utils.py
+++ b/erpnext/e_commerce/redisearch_utils.py
@@ -38,7 +38,7 @@
 		out = cache.execute_command("MODULE LIST")
 
 		parsed_output = " ".join(
-			(" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
+			(" ".join([frappe.as_unicode(s) for s in o if not isinstance(s, int)]) for o in out)
 		)
 		return "search" in parsed_output
 	except Exception:
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 1c4bbbc..7d7f65d 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -392,9 +392,12 @@
 
 scheduler_events = {
 	"cron": {
+		"0/5 * * * *": [
+			"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
+		],
 		"0/30 * * * *": [
 			"erpnext.utilities.doctype.video.video.update_youtube_data",
-		]
+		],
 	},
 	"all": [
 		"erpnext.projects.doctype.project.project.project_status_update_reminder",
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index 85997a4..ee00e67 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -173,7 +173,7 @@
 					date: frm.doc.from_date,
 					to_date: frm.doc.to_date,
 					leave_type: frm.doc.leave_type,
-					consider_all_leaves_in_the_allocation_period: true
+					consider_all_leaves_in_the_allocation_period: 1
 				},
 				callback: function (r) {
 					if (!r.exc && r.message) {
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 53c5df4..43c2bb3 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -757,22 +757,6 @@
 	leave_allocation = {}
 	for d in allocation_records:
 		allocation = allocation_records.get(d, frappe._dict())
-
-		total_allocated_leaves = (
-			frappe.db.get_value(
-				"Leave Allocation",
-				{
-					"from_date": ("<=", date),
-					"to_date": (">=", date),
-					"employee": employee,
-					"leave_type": allocation.leave_type,
-					"docstatus": 1,
-				},
-				"SUM(total_leaves_allocated)",
-			)
-			or 0
-		)
-
 		remaining_leaves = get_leave_balance_on(
 			employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True
 		)
@@ -782,10 +766,11 @@
 		leaves_pending = get_leaves_pending_approval_for_period(
 			employee, d, allocation.from_date, end_date
 		)
+		expired_leaves = allocation.total_leaves_allocated - (remaining_leaves + leaves_taken)
 
 		leave_allocation[d] = {
-			"total_leaves": total_allocated_leaves,
-			"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
+			"total_leaves": allocation.total_leaves_allocated,
+			"expired_leaves": expired_leaves if expired_leaves > 0 else 0,
 			"leaves_taken": leaves_taken,
 			"leaves_pending_approval": leaves_pending,
 			"remaining_leaves": remaining_leaves,
@@ -830,7 +815,7 @@
 	allocation_records = get_leave_allocation_records(employee, date, leave_type)
 	allocation = allocation_records.get(leave_type, frappe._dict())
 
-	end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
+	end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date
 	cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
 
 	leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 7506c61..27c5410 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -76,7 +76,14 @@
 
 class TestLeaveApplication(unittest.TestCase):
 	def setUp(self):
-		for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
+		for dt in [
+			"Leave Application",
+			"Leave Allocation",
+			"Salary Slip",
+			"Leave Ledger Entry",
+			"Leave Period",
+			"Leave Policy Assignment",
+		]:
 			frappe.db.delete(dt)
 
 		frappe.set_user("Administrator")
@@ -702,59 +709,24 @@
 		self.assertEqual(details.leave_balance, 30)
 
 	def test_earned_leaves_creation(self):
-
-		frappe.db.sql("""delete from `tabLeave Period`""")
-		frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
-		frappe.db.sql("""delete from `tabLeave Allocation`""")
-		frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
+		from erpnext.hr.utils import allocate_earned_leaves
 
 		leave_period = get_leave_period()
 		employee = get_employee()
 		leave_type = "Test Earned Leave Type"
-		frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1)
-		frappe.get_doc(
-			dict(
-				leave_type_name=leave_type,
-				doctype="Leave Type",
-				is_earned_leave=1,
-				earned_leave_frequency="Monthly",
-				rounding=0.5,
-				max_leaves_allowed=6,
-			)
-		).insert()
+		make_policy_assignment(employee, leave_type, leave_period)
 
-		leave_policy = frappe.get_doc(
-			{
-				"doctype": "Leave Policy",
-				"title": "Test Leave Policy",
-				"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
-			}
-		).insert()
-
-		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)
-		)
-
-		from erpnext.hr.utils import allocate_earned_leaves
-
-		i = 0
-		while i < 14:
+		for i in range(0, 14):
 			allocate_earned_leaves()
-			i += 1
+
 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
 
 		# validate earned leaves creation without maximum leaves
 		frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
-		i = 0
-		while i < 6:
+
+		for i in range(0, 6):
 			allocate_earned_leaves()
-			i += 1
+
 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
 
 	# test to not consider current leave in leave balance while submitting
@@ -971,6 +943,54 @@
 		self.assertEqual(leave_allocation["remaining_leaves"], 26)
 
 	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+	def test_get_earned_leave_details_for_dashboard(self):
+		from erpnext.hr.utils import allocate_earned_leaves
+
+		leave_period = get_leave_period()
+		employee = get_employee()
+		leave_type = "Test Earned Leave Type"
+		leave_policy_assignments = make_policy_assignment(employee, leave_type, leave_period)
+		allocation = frappe.db.get_value(
+			"Leave Allocation",
+			{"leave_policy_assignment": leave_policy_assignments[0]},
+			"name",
+		)
+		allocation = frappe.get_doc("Leave Allocation", allocation)
+		allocation.new_leaves_allocated = 2
+		allocation.save()
+
+		for i in range(0, 6):
+			allocate_earned_leaves()
+
+		first_sunday = get_first_sunday(self.holiday_list)
+		make_leave_application(
+			employee.name, add_days(first_sunday, 1), add_days(first_sunday, 1), leave_type
+		)
+
+		details = get_leave_details(employee.name, allocation.from_date)
+		leave_allocation = details["leave_allocation"][leave_type]
+		expected = {
+			"total_leaves": 2.0,
+			"expired_leaves": 0.0,
+			"leaves_taken": 1.0,
+			"leaves_pending_approval": 0.0,
+			"remaining_leaves": 1.0,
+		}
+		self.assertEqual(leave_allocation, expected)
+
+		details = get_leave_details(employee.name, getdate())
+		leave_allocation = details["leave_allocation"][leave_type]
+
+		expected = {
+			"total_leaves": 5.0,
+			"expired_leaves": 0.0,
+			"leaves_taken": 1.0,
+			"leaves_pending_approval": 0.0,
+			"remaining_leaves": 4.0,
+		}
+		self.assertEqual(leave_allocation, expected)
+
+	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
 	def test_get_leave_allocation_records(self):
 		employee = get_employee()
 		leave_type = create_leave_type(
@@ -1100,3 +1120,36 @@
 	)[0][0]
 
 	return first_sunday
+
+
+def make_policy_assignment(employee, leave_type, leave_period):
+	frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
+	frappe.get_doc(
+		dict(
+			leave_type_name=leave_type,
+			doctype="Leave Type",
+			is_earned_leave=1,
+			earned_leave_frequency="Monthly",
+			rounding=0.5,
+			max_leaves_allowed=6,
+		)
+	).insert()
+
+	leave_policy = frappe.get_doc(
+		{
+			"doctype": "Leave Policy",
+			"title": "Test Leave Policy",
+			"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
+		}
+	).insert()
+
+	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)
+	)
+	return leave_policy_assignments
diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
index 940a1bb..38328e6 100644
--- a/erpnext/loan_management/doctype/loan/loan.js
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -93,6 +93,12 @@
 					frm.trigger("make_loan_refund");
 				},__('Create'));
 			}
+
+			if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && !frm.doc.is_secured_loan) {
+				frm.add_custom_button(__('Close Loan'), function() {
+					frm.trigger("close_unsecured_term_loan");
+				},__('Status'));
+			}
 		}
 		frm.trigger("toggle_fields");
 	},
@@ -174,6 +180,18 @@
 		})
 	},
 
+	close_unsecured_term_loan: function(frm) {
+		frappe.call({
+			args: {
+				"loan": frm.doc.name
+			},
+			method: "erpnext.loan_management.doctype.loan.loan.close_unsecured_term_loan",
+			callback: function () {
+				frm.refresh();
+			}
+		})
+	},
+
 	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.py b/erpnext/loan_management/doctype/loan/loan.py
index 3b76ba4..ac8b362 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -60,11 +60,11 @@
 				)
 
 	def validate_cost_center(self):
-		if not self.cost_center and self.rate_of_interest != 0:
+		if not self.cost_center and self.rate_of_interest != 0.0:
 			self.cost_center = frappe.db.get_value("Company", self.company, "cost_center")
 
-		if not self.cost_center:
-			frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
+			if not self.cost_center:
+				frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
 
 	def on_submit(self):
 		self.link_loan_security_pledge()
@@ -342,6 +342,22 @@
 		return loan.as_dict()
 
 
+@frappe.whitelist()
+def close_unsecured_term_loan(loan):
+	loan_details = frappe.db.get_value(
+		"Loan", {"name": loan}, ["status", "is_term_loan", "is_secured_loan"], as_dict=1
+	)
+
+	if (
+		loan_details.status == "Loan Closure Requested"
+		and loan_details.is_term_loan
+		and not loan_details.is_secured_loan
+	):
+		frappe.db.set_value("Loan", loan, "status", "Closed")
+	else:
+		frappe.throw(_("Cannot close this loan until full repayment"))
+
+
 def close_loan(loan, total_amount_paid):
 	frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
 	frappe.db.set_value("Loan", loan, "status", "Closed")
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index d743798..ecad41f 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -81,7 +81,7 @@
 			}
 		)
 
-		if (!frm.doc.__islocal && frm.doc.docstatus<2) {
+		if (!frm.is_new() && frm.doc.docstatus<2) {
 			frm.add_custom_button(__("Update Cost"), function() {
 				frm.events.update_cost(frm, true);
 			});
@@ -93,10 +93,12 @@
 			});
 		}
 
-		frm.add_custom_button(__("New Version"), function() {
-			let new_bom = frappe.model.copy_doc(frm.doc);
-			frappe.set_route("Form", "BOM", new_bom.name);
-		});
+		if (!frm.is_new() && !frm.doc.docstatus == 0) {
+			frm.add_custom_button(__("New Version"), function() {
+				let new_bom = frappe.model.copy_doc(frm.doc);
+				frappe.set_route("Form", "BOM", new_bom.name);
+			});
+		}
 
 		if(frm.doc.docstatus==1) {
 			frm.add_custom_button(__("Work Order"), function() {
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 6376359..631548b 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1,11 +1,11 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
 import functools
 import re
 from collections import deque
 from operator import itemgetter
-from typing import List
+from typing import Dict, List
 
 import frappe
 from frappe import _
@@ -189,6 +189,7 @@
 		self.validate_transfer_against()
 		self.set_routing_operations()
 		self.validate_operations()
+		self.update_exploded_items(save=False)
 		self.calculate_cost()
 		self.update_stock_qty()
 		self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
@@ -386,40 +387,14 @@
 
 		existing_bom_cost = self.total_cost
 
-		for d in self.get("items"):
-			if not d.item_code:
-				continue
-
-			rate = self.get_rm_rate(
-				{
-					"company": self.company,
-					"item_code": d.item_code,
-					"bom_no": d.bom_no,
-					"qty": d.qty,
-					"uom": d.uom,
-					"stock_uom": d.stock_uom,
-					"conversion_factor": d.conversion_factor,
-					"sourced_by_supplier": d.sourced_by_supplier,
-				}
-			)
-
-			if rate:
-				d.rate = rate
-			d.amount = flt(d.rate) * flt(d.qty)
-			d.base_rate = flt(d.rate) * flt(self.conversion_rate)
-			d.base_amount = flt(d.amount) * flt(self.conversion_rate)
-
-			if save:
-				d.db_update()
-
 		if self.docstatus == 1:
 			self.flags.ignore_validate_update_after_submit = True
-			self.calculate_cost(update_hour_rate)
+
+		self.calculate_cost(save_updates=save, update_hour_rate=update_hour_rate)
+
 		if save:
 			self.db_update()
 
-		self.update_exploded_items(save=save)
-
 		# update parent BOMs
 		if self.total_cost != existing_bom_cost and update_parent:
 			parent_boms = frappe.db.sql_list(
@@ -608,11 +583,15 @@
 		bom_list.reverse()
 		return bom_list
 
-	def calculate_cost(self, update_hour_rate=False):
+	def calculate_cost(self, save_updates=False, update_hour_rate=False):
 		"""Calculate bom totals"""
 		self.calculate_op_cost(update_hour_rate)
-		self.calculate_rm_cost()
-		self.calculate_sm_cost()
+		self.calculate_rm_cost(save=save_updates)
+		self.calculate_sm_cost(save=save_updates)
+		if save_updates:
+			# not via doc event, table is not regenerated and needs updation
+			self.calculate_exploded_cost()
+
 		self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
 		self.base_total_cost = (
 			self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
@@ -654,12 +633,26 @@
 		if update_hour_rate:
 			row.db_update()
 
-	def calculate_rm_cost(self):
+	def calculate_rm_cost(self, save=False):
 		"""Fetch RM rate as per today's valuation rate and calculate totals"""
 		total_rm_cost = 0
 		base_total_rm_cost = 0
 
 		for d in self.get("items"):
+			old_rate = d.rate
+			d.rate = self.get_rm_rate(
+				{
+					"company": self.company,
+					"item_code": d.item_code,
+					"bom_no": d.bom_no,
+					"qty": d.qty,
+					"uom": d.uom,
+					"stock_uom": d.stock_uom,
+					"conversion_factor": d.conversion_factor,
+					"sourced_by_supplier": d.sourced_by_supplier,
+				}
+			)
+
 			d.base_rate = flt(d.rate) * flt(self.conversion_rate)
 			d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
 			d.base_amount = d.amount * flt(self.conversion_rate)
@@ -669,11 +662,13 @@
 
 			total_rm_cost += d.amount
 			base_total_rm_cost += d.base_amount
+			if save and (old_rate != d.rate):
+				d.db_update()
 
 		self.raw_material_cost = total_rm_cost
 		self.base_raw_material_cost = base_total_rm_cost
 
-	def calculate_sm_cost(self):
+	def calculate_sm_cost(self, save=False):
 		"""Fetch RM rate as per today's valuation rate and calculate totals"""
 		total_sm_cost = 0
 		base_total_sm_cost = 0
@@ -688,10 +683,45 @@
 			)
 			total_sm_cost += d.amount
 			base_total_sm_cost += d.base_amount
+			if save:
+				d.db_update()
 
 		self.scrap_material_cost = total_sm_cost
 		self.base_scrap_material_cost = base_total_sm_cost
 
+	def calculate_exploded_cost(self):
+		"Set exploded row cost from it's parent BOM."
+		rm_rate_map = self.get_rm_rate_map()
+
+		for row in self.get("exploded_items"):
+			old_rate = flt(row.rate)
+			row.rate = rm_rate_map.get(row.item_code)
+			row.amount = flt(row.stock_qty) * flt(row.rate)
+
+			if old_rate != row.rate:
+				# Only db_update if changed
+				row.db_update()
+
+	def get_rm_rate_map(self) -> Dict[str, float]:
+		"Create Raw Material-Rate map for Exploded Items. Fetch rate from Items table or Subassembly BOM."
+		rm_rate_map = {}
+
+		for item in self.get("items"):
+			if item.bom_no:
+				# Get Item-Rate from Subassembly BOM
+				explosion_items = frappe.get_all(
+					"BOM Explosion Item",
+					filters={"parent": item.bom_no},
+					fields=["item_code", "rate"],
+					order_by=None,  # to avoid sort index creation at db level (granular change)
+				)
+				explosion_item_rate = {item.item_code: flt(item.rate) for item in explosion_items}
+				rm_rate_map.update(explosion_item_rate)
+			else:
+				rm_rate_map[item.item_code] = flt(item.base_rate) / flt(item.conversion_factor or 1.0)
+
+		return rm_rate_map
+
 	def update_exploded_items(self, save=True):
 		"""Update Flat BOM, following will be correct data"""
 		self.get_exploded_items()
@@ -902,44 +932,46 @@
 	return flt(rate)
 
 
-def get_valuation_rate(args):
-	"""Get weighted average of valuation rate from all warehouses"""
+def get_valuation_rate(data):
+	"""
+	1) Get average valuation rate from all warehouses
+	2) If no value, get last valuation rate from SLE
+	3) If no value, get valuation rate from Item
+	"""
+	from frappe.query_builder.functions import Sum
 
-	total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
-	item_bins = frappe.db.sql(
-		"""
-		select
-			bin.actual_qty, bin.stock_value
-		from
-			`tabBin` bin, `tabWarehouse` warehouse
-		where
-			bin.item_code=%(item)s
-			and bin.warehouse = warehouse.name
-			and warehouse.company=%(company)s""",
-		{"item": args["item_code"], "company": args["company"]},
-		as_dict=1,
-	)
+	item_code, company = data.get("item_code"), data.get("company")
+	valuation_rate = 0.0
 
-	for d in item_bins:
-		total_qty += flt(d.actual_qty)
-		total_value += flt(d.stock_value)
+	bin_table = frappe.qb.DocType("Bin")
+	wh_table = frappe.qb.DocType("Warehouse")
+	item_valuation = (
+		frappe.qb.from_(bin_table)
+		.join(wh_table)
+		.on(bin_table.warehouse == wh_table.name)
+		.select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate"))
+		.where((bin_table.item_code == item_code) & (wh_table.company == company))
+	).run(as_dict=True)[0]
 
-	if total_qty:
-		valuation_rate = total_value / total_qty
+	valuation_rate = item_valuation.get("valuation_rate")
 
-	if valuation_rate <= 0:
-		last_valuation_rate = frappe.db.sql(
-			"""select valuation_rate
-			from `tabStock Ledger Entry`
-			where item_code = %s and valuation_rate > 0 and is_cancelled = 0
-			order by posting_date desc, posting_time desc, creation desc limit 1""",
-			args["item_code"],
-		)
+	if (valuation_rate is not None) and valuation_rate <= 0:
+		# Explicit null value check. If None, Bins don't exist, neither does SLE
+		sle = frappe.qb.DocType("Stock Ledger Entry")
+		last_val_rate = (
+			frappe.qb.from_(sle)
+			.select(sle.valuation_rate)
+			.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
+			.orderby(sle.posting_date, order=frappe.qb.desc)
+			.orderby(sle.posting_time, order=frappe.qb.desc)
+			.orderby(sle.creation, order=frappe.qb.desc)
+			.limit(1)
+		).run(as_dict=True)
 
-		valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
+		valuation_rate = flt(last_val_rate[0].get("valuation_rate")) if last_val_rate else 0
 
 	if not valuation_rate:
-		valuation_rate = frappe.db.get_value("Item", args["item_code"], "valuation_rate")
+		valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
 
 	return flt(valuation_rate)
 
@@ -1125,39 +1157,6 @@
 		return bom_items
 
 
-def get_boms_in_bottom_up_order(bom_no=None):
-	def _get_parent(bom_no):
-		return frappe.db.sql_list(
-			"""
-			select distinct bom_item.parent from `tabBOM Item` bom_item
-			where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
-				and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
-		""",
-			bom_no,
-		)
-
-	count = 0
-	bom_list = []
-	if bom_no:
-		bom_list.append(bom_no)
-	else:
-		# get all leaf BOMs
-		bom_list = frappe.db.sql_list(
-			"""select name from `tabBOM` bom
-			where docstatus=1 and is_active=1
-				and not exists(select bom_no from `tabBOM Item`
-					where parent=bom.name and ifnull(bom_no, '')!='')"""
-		)
-
-	while count < len(bom_list):
-		for child_bom in _get_parent(bom_list[count]):
-			if child_bom not in bom_list:
-				bom_list.append(child_bom)
-		count += 1
-
-	return bom_list
-
-
 def add_additional_cost(stock_entry, work_order):
 	# Add non stock items cost in the additional cost
 	stock_entry.additional_costs = []
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index f235e44..182a20c 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -11,7 +11,9 @@
 
 from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
 from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom
-from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
+from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
+	update_cost_in_all_boms_in_test,
+)
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
 	create_stock_reconciliation,
@@ -69,26 +71,31 @@
 
 	def test_update_bom_cost_in_all_boms(self):
 		# get current rate for '_Test Item 2'
-		rm_rate = frappe.db.sql(
-			"""select rate from `tabBOM Item`
-			where parent='BOM-_Test Item Home Desktop Manufactured-001'
-			and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'"""
+		bom_rates = frappe.db.get_values(
+			"BOM Item",
+			{
+				"parent": "BOM-_Test Item Home Desktop Manufactured-001",
+				"item_code": "_Test Item 2",
+				"docstatus": 1,
+			},
+			fieldname=["rate", "base_rate"],
+			as_dict=True,
 		)
-		rm_rate = rm_rate[0][0] if rm_rate else 0
+		rm_base_rate = bom_rates[0].get("base_rate") if bom_rates else 0
 
 		# Reset item valuation rate
-		reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10)
+		reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_base_rate + 10)
 
 		# update cost of all BOMs based on latest valuation rate
-		update_cost()
+		update_cost_in_all_boms_in_test()
 
 		# check if new valuation rate updated in all BOMs
 		for d in frappe.db.sql(
-			"""select rate from `tabBOM Item`
+			"""select base_rate from `tabBOM Item`
 			where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""",
 			as_dict=1,
 		):
-			self.assertEqual(d.rate, rm_rate + 10)
+			self.assertEqual(d.base_rate, rm_base_rate + 10)
 
 	def test_bom_cost(self):
 		bom = frappe.copy_doc(test_records[2])
diff --git a/erpnext/manufacturing/doctype/bom/test_records.json b/erpnext/manufacturing/doctype/bom/test_records.json
index 25730f9..507d319 100644
--- a/erpnext/manufacturing/doctype/bom/test_records.json
+++ b/erpnext/manufacturing/doctype/bom/test_records.json
@@ -32,6 +32,7 @@
   "is_active": 1,
   "is_default": 1,
   "item": "_Test Item Home Desktop Manufactured",
+  "company": "_Test Company",
   "quantity": 1.0
  },
  {
diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
index f01d856..9b1db63 100644
--- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
+++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
@@ -169,13 +169,15 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-10-08 16:21:29.386212",
+ "modified": "2022-05-27 13:42:23.305455",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "BOM Explosion Item",
+ "naming_rule": "Random",
  "owner": "Administrator",
  "permissions": [],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_update_batch/__init__.py b/erpnext/manufacturing/doctype/bom_update_batch/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_batch/__init__.py
diff --git a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json
new file mode 100644
index 0000000..83b54d3
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "autoname": "autoincrement",
+ "creation": "2022-05-31 17:34:39.825537",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+  "level",
+  "batch_no",
+  "boms_updated",
+  "status"
+ ],
+ "fields": [
+  {
+   "fieldname": "level",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Level"
+  },
+  {
+   "fieldname": "batch_no",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Batch No."
+  },
+  {
+   "fieldname": "boms_updated",
+   "fieldtype": "Long Text",
+   "hidden": 1,
+   "in_list_view": 1,
+   "label": "BOMs Updated"
+  },
+  {
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "label": "Status",
+   "options": "Pending\nCompleted",
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-06-06 14:50:35.161062",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "BOM Update Batch",
+ "naming_rule": "Autoincrement",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.py b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.py
new file mode 100644
index 0000000..f952e43
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BOMUpdateBatch(Document):
+	pass
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
index 98c1acb..c32e383 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
@@ -13,6 +13,10 @@
   "update_type",
   "status",
   "error_log",
+  "progress_section",
+  "current_level",
+  "processed_boms",
+  "bom_batches",
   "amended_from"
  ],
  "fields": [
@@ -63,13 +67,36 @@
    "fieldtype": "Link",
    "label": "Error Log",
    "options": "Error Log"
+  },
+  {
+   "collapsible": 1,
+   "depends_on": "eval: doc.update_type == \"Update Cost\"",
+   "fieldname": "progress_section",
+   "fieldtype": "Section Break",
+   "label": "Progress"
+  },
+  {
+   "fieldname": "processed_boms",
+   "fieldtype": "Long Text",
+   "hidden": 1,
+   "label": "Processed BOMs"
+  },
+  {
+   "fieldname": "bom_batches",
+   "fieldtype": "Table",
+   "options": "BOM Update Batch"
+  },
+  {
+   "fieldname": "current_level",
+   "fieldtype": "Int",
+   "label": "Current Level"
   }
  ],
  "in_create": 1,
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-03-31 12:51:44.885102",
+ "modified": "2022-06-06 15:15:23.883251",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "BOM Update Log",
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
index c0770fa..9c9c240 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
@@ -1,13 +1,20 @@
 # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
-from typing import Dict, List, Literal, Optional
+import json
+from typing import Any, Dict, List, Optional, Tuple, Union
 
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import cstr, flt
+from frappe.utils import cint, cstr
 
-from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
+from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import (
+	get_leaf_boms,
+	get_next_higher_level_boms,
+	handle_exception,
+	replace_bom,
+	set_values_in_log,
+)
 
 
 class BOMMissingError(frappe.ValidationError):
@@ -20,6 +27,8 @@
 			self.validate_boms_are_specified()
 			self.validate_same_bom()
 			self.validate_bom_items()
+		else:
+			self.validate_bom_cost_update_in_progress()
 
 		self.status = "Queued"
 
@@ -42,123 +51,184 @@
 		if current_bom_item != new_bom_item:
 			frappe.throw(_("The selected BOMs are not for the same item"))
 
-	def on_submit(self):
-		if frappe.flags.in_test:
-			return
+	def validate_bom_cost_update_in_progress(self):
+		"If another Cost Updation Log is still in progress, dont make new ones."
 
+		wip_log = frappe.get_all(
+			"BOM Update Log",
+			{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
+			limit_page_length=1,
+		)
+		if wip_log:
+			log_link = frappe.utils.get_link_to_form("BOM Update Log", wip_log[0].name)
+			frappe.throw(
+				_("BOM Updation already in progress. Please wait until {0} is complete.").format(log_link),
+				title=_("Note"),
+			)
+
+	def on_submit(self):
 		if self.update_type == "Replace BOM":
 			boms = {"current_bom": self.current_bom, "new_bom": self.new_bom}
 			frappe.enqueue(
-				method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
+				method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_replace_bom_job",
 				doc=self,
 				boms=boms,
 				timeout=40000,
+				now=frappe.flags.in_test,
 			)
 		else:
-			frappe.enqueue(
-				method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
-				doc=self,
-				update_type="Update Cost",
-				timeout=40000,
-			)
+			process_boms_cost_level_wise(self)
 
 
-def replace_bom(boms: Dict) -> None:
-	"""Replace current BOM with new BOM in parent BOMs."""
-	current_bom = boms.get("current_bom")
-	new_bom = boms.get("new_bom")
-
-	unit_cost = get_new_bom_unit_cost(new_bom)
-	update_new_bom_in_bom_items(unit_cost, current_bom, new_bom)
-
-	frappe.cache().delete_key("bom_children")
-	parent_boms = get_parent_boms(new_bom)
-
-	for bom in parent_boms:
-		bom_obj = frappe.get_doc("BOM", bom)
-		# this is only used for versioning and we do not want
-		# to make separate db calls by using load_doc_before_save
-		# which proves to be expensive while doing bulk replace
-		bom_obj._doc_before_save = bom_obj
-		bom_obj.update_exploded_items()
-		bom_obj.calculate_cost()
-		bom_obj.update_parent_cost()
-		bom_obj.db_update()
-		if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version:
-			bom_obj.save_version()
-
-
-def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
-	bom_item = frappe.qb.DocType("BOM Item")
-	(
-		frappe.qb.update(bom_item)
-		.set(bom_item.bom_no, new_bom)
-		.set(bom_item.rate, unit_cost)
-		.set(bom_item.amount, (bom_item.stock_qty * unit_cost))
-		.where(
-			(bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")
-		)
-	).run()
-
-
-def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
-	bom_list = bom_list or []
-	bom_item = frappe.qb.DocType("BOM Item")
-
-	parents = (
-		frappe.qb.from_(bom_item)
-		.select(bom_item.parent)
-		.where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM"))
-		.run(as_dict=True)
-	)
-
-	for d in parents:
-		if new_bom == d.parent:
-			frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
-
-		bom_list.append(d.parent)
-		get_parent_boms(d.parent, bom_list)
-
-	return list(set(bom_list))
-
-
-def get_new_bom_unit_cost(new_bom: str) -> float:
-	bom = frappe.qb.DocType("BOM")
-	new_bom_unitcost = (
-		frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run()
-	)
-
-	return flt(new_bom_unitcost[0][0])
-
-
-def run_bom_job(
+def run_replace_bom_job(
 	doc: "BOMUpdateLog",
 	boms: Optional[Dict[str, str]] = None,
-	update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
 ) -> None:
 	try:
 		doc.db_set("status", "In Progress")
+
 		if not frappe.flags.in_test:
 			frappe.db.commit()
 
 		frappe.db.auto_commit_on_many_writes = 1
-
 		boms = frappe._dict(boms or {})
-
-		if update_type == "Replace BOM":
-			replace_bom(boms)
-		else:
-			update_cost()
+		replace_bom(boms, doc.name)
 
 		doc.db_set("status", "Completed")
-
 	except Exception:
-		frappe.db.rollback()
-		error_log = doc.log_error("BOM Update Tool Error")
-
-		doc.db_set("status", "Failed")
-		doc.db_set("error_log", error_log.name)
-
+		handle_exception(doc)
 	finally:
 		frappe.db.auto_commit_on_many_writes = 0
-		frappe.db.commit()  # nosemgrep
+
+		if not frappe.flags.in_test:
+			frappe.db.commit()  # nosemgrep
+
+
+def process_boms_cost_level_wise(
+	update_doc: "BOMUpdateLog", parent_boms: List[str] = None
+) -> Union[None, Tuple]:
+	"Queue jobs at the start of new BOM Level in 'Update Cost' Jobs."
+
+	current_boms = {}
+	values = {}
+
+	if update_doc.status == "Queued":
+		# First level yet to process. On Submit.
+		current_level = 0
+		current_boms = get_leaf_boms()
+		values = {
+			"processed_boms": json.dumps({}),
+			"status": "In Progress",
+			"current_level": current_level,
+		}
+	else:
+		# Resume next level. via Cron Job.
+		if not parent_boms:
+			return
+
+		current_level = cint(update_doc.current_level) + 1
+
+		# Process the next level BOMs. Stage parents as current BOMs.
+		current_boms = parent_boms.copy()
+		values = {"current_level": current_level}
+
+	set_values_in_log(update_doc.name, values, commit=True)
+	queue_bom_cost_jobs(current_boms, update_doc, current_level)
+
+
+def queue_bom_cost_jobs(
+	current_boms_list: List[str], update_doc: "BOMUpdateLog", current_level: int
+) -> None:
+	"Queue batches of 20k BOMs of the same level to process parallelly"
+	batch_no = 0
+
+	while current_boms_list:
+		batch_no += 1
+		batch_size = 20_000
+		boms_to_process = current_boms_list[:batch_size]  # slice out batch of 20k BOMs
+
+		# update list to exclude 20K (queued) BOMs
+		current_boms_list = current_boms_list[batch_size:] if len(current_boms_list) > batch_size else []
+
+		batch_row = update_doc.append(
+			"bom_batches", {"level": current_level, "batch_no": batch_no, "status": "Pending"}
+		)
+		batch_row.db_insert()
+
+		frappe.enqueue(
+			method="erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils.update_cost_in_level",
+			doc=update_doc,
+			bom_list=boms_to_process,
+			batch_name=batch_row.name,
+			queue="long",
+			now=frappe.flags.in_test,
+		)
+
+
+def resume_bom_cost_update_jobs():
+	"""
+	1. Checks for In Progress BOM Update Log.
+	2. Checks if this job has completed the _current level_.
+	3. If current level is complete, get parent BOMs and start next level.
+	4. If no parents, mark as Complete.
+	5. If current level is WIP, skip the Log.
+
+	Called every 5 minutes via Cron job.
+	"""
+
+	in_progress_logs = frappe.db.get_all(
+		"BOM Update Log",
+		{"update_type": "Update Cost", "status": "In Progress"},
+		["name", "processed_boms", "current_level"],
+	)
+	if not in_progress_logs:
+		return
+
+	for log in in_progress_logs:
+		# check if all log batches of current level are processed
+		bom_batches = frappe.db.get_all(
+			"BOM Update Batch",
+			{"parent": log.name, "level": log.current_level},
+			["name", "boms_updated", "status"],
+		)
+		incomplete_level = any(row.get("status") == "Pending" for row in bom_batches)
+		if not bom_batches or incomplete_level:
+			continue
+
+		# Prep parent BOMs & updated processed BOMs for next level
+		current_boms, processed_boms = get_processed_current_boms(log, bom_batches)
+		parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms)
+
+		# Unset processed BOMs if log is complete, it is used for next level BOMs
+		set_values_in_log(
+			log.name,
+			values={
+				"processed_boms": json.dumps([] if not parent_boms else processed_boms),
+				"status": "Completed" if not parent_boms else "In Progress",
+			},
+			commit=True,
+		)
+
+		if parent_boms:  # there is a next level to process
+			process_boms_cost_level_wise(
+				update_doc=frappe.get_doc("BOM Update Log", log.name), parent_boms=parent_boms
+			)
+
+
+def get_processed_current_boms(
+	log: Dict[str, Any], bom_batches: Dict[str, Any]
+) -> Tuple[List[str], Dict[str, Any]]:
+	"""
+	Aggregate all BOMs from BOM Update Batch rows into 'processed_boms' field
+	and into current boms list.
+	"""
+	processed_boms = json.loads(log.processed_boms) if log.processed_boms else {}
+	current_boms = []
+
+	for row in bom_batches:
+		boms_updated = json.loads(row.boms_updated)
+		current_boms.extend(boms_updated)
+		boms_updated_dict = {bom: True for bom in boms_updated}
+		processed_boms.update(boms_updated_dict)
+
+	return current_boms, processed_boms
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py
new file mode 100644
index 0000000..af115e3
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py
@@ -0,0 +1,225 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import copy
+import json
+from collections import defaultdict
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
+
+if TYPE_CHECKING:
+	from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog
+
+import frappe
+from frappe import _
+
+
+def replace_bom(boms: Dict, log_name: str) -> None:
+	"Replace current BOM with new BOM in parent BOMs."
+
+	current_bom = boms.get("current_bom")
+	new_bom = boms.get("new_bom")
+
+	unit_cost = get_bom_unit_cost(new_bom)
+	update_new_bom_in_bom_items(unit_cost, current_bom, new_bom)
+
+	frappe.cache().delete_key("bom_children")
+	parent_boms = get_ancestor_boms(new_bom)
+
+	for bom in parent_boms:
+		bom_obj = frappe.get_doc("BOM", bom)
+		# this is only used for versioning and we do not want
+		# to make separate db calls by using load_doc_before_save
+		# which proves to be expensive while doing bulk replace
+		bom_obj._doc_before_save = copy.deepcopy(bom_obj)
+		bom_obj.update_exploded_items()
+		bom_obj.calculate_cost()
+		bom_obj.update_parent_cost()
+		bom_obj.db_update()
+		bom_obj.flags.updater_reference = {
+			"doctype": "BOM Update Log",
+			"docname": log_name,
+			"label": _("via BOM Update Tool"),
+		}
+		bom_obj.save_version()
+
+
+def update_cost_in_level(
+	doc: "BOMUpdateLog", bom_list: List[str], batch_name: Union[int, str]
+) -> None:
+	"Updates Cost for BOMs within a given level. Runs via background jobs."
+
+	try:
+		status = frappe.db.get_value("BOM Update Log", doc.name, "status")
+		if status == "Failed":
+			return
+
+		update_cost_in_boms(bom_list=bom_list)  # main updation logic
+
+		bom_batch = frappe.qb.DocType("BOM Update Batch")
+		(
+			frappe.qb.update(bom_batch)
+			.set(bom_batch.boms_updated, json.dumps(bom_list))
+			.set(bom_batch.status, "Completed")
+			.where(bom_batch.name == batch_name)
+		).run()
+	except Exception:
+		handle_exception(doc)
+	finally:
+		if not frappe.flags.in_test:
+			frappe.db.commit()  # nosemgrep
+
+
+def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
+	"Recursively get all ancestors of BOM."
+
+	bom_list = bom_list or []
+	bom_item = frappe.qb.DocType("BOM Item")
+
+	parents = (
+		frappe.qb.from_(bom_item)
+		.select(bom_item.parent)
+		.where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM"))
+		.run(as_dict=True)
+	)
+
+	for d in parents:
+		if new_bom == d.parent:
+			frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
+
+		bom_list.append(d.parent)
+		get_ancestor_boms(d.parent, bom_list)
+
+	return list(set(bom_list))
+
+
+def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
+	bom_item = frappe.qb.DocType("BOM Item")
+	(
+		frappe.qb.update(bom_item)
+		.set(bom_item.bom_no, new_bom)
+		.set(bom_item.rate, unit_cost)
+		.set(bom_item.amount, (bom_item.stock_qty * unit_cost))
+		.where(
+			(bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")
+		)
+	).run()
+
+
+def get_bom_unit_cost(bom_name: str) -> float:
+	bom = frappe.qb.DocType("BOM")
+	new_bom_unitcost = (
+		frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == bom_name).run()
+	)
+
+	return frappe.utils.flt(new_bom_unitcost[0][0])
+
+
+def update_cost_in_boms(bom_list: List[str]) -> None:
+	"Updates cost in given BOMs. Returns current and total updated BOMs."
+
+	for index, bom in enumerate(bom_list):
+		bom_doc = frappe.get_doc("BOM", bom, for_update=True)
+		bom_doc.calculate_cost(save_updates=True, update_hour_rate=True)
+		bom_doc.db_update()
+
+		if (index % 50 == 0) and not frappe.flags.in_test:
+			frappe.db.commit()  # nosemgrep
+
+
+def get_next_higher_level_boms(
+	child_boms: List[str], processed_boms: Dict[str, bool]
+) -> List[str]:
+	"Generate immediate higher level dependants with no unresolved dependencies (children)."
+
+	def _all_children_are_processed(parent_bom):
+		child_boms = dependency_map.get(parent_bom)
+		return all(processed_boms.get(bom) for bom in child_boms)
+
+	dependants_map, dependency_map = _generate_dependence_map()
+
+	dependants = []
+	for bom in child_boms:
+		# generate list of immediate dependants
+		parents = dependants_map.get(bom) or []
+		dependants.extend(parents)
+
+	dependants = set(dependants)  # remove duplicates
+	resolved_dependants = set()
+
+	# consider only if children are all resolved
+	for parent_bom in dependants:
+		if _all_children_are_processed(parent_bom):
+			resolved_dependants.add(parent_bom)
+
+	return list(resolved_dependants)
+
+
+def get_leaf_boms() -> List[str]:
+	"Get BOMs that have no dependencies."
+
+	return frappe.db.sql_list(
+		"""select name from `tabBOM` bom
+		where docstatus=1 and is_active=1
+			and not exists(select bom_no from `tabBOM Item`
+				where parent=bom.name and ifnull(bom_no, '')!='')"""
+	)
+
+
+def _generate_dependence_map() -> defaultdict:
+	"""
+	Generate maps such as: { BOM-1: [Dependant-BOM-1, Dependant-BOM-2, ..] }.
+	Here BOM-1 is the leaf/lower level node/dependency.
+	The list contains one level higher nodes/dependants that depend on BOM-1.
+
+	Generate and return the reverse as well.
+	"""
+
+	bom = frappe.qb.DocType("BOM")
+	bom_item = frappe.qb.DocType("BOM Item")
+
+	bom_items = (
+		frappe.qb.from_(bom_item)
+		.join(bom)
+		.on(bom_item.parent == bom.name)
+		.select(bom_item.bom_no, bom_item.parent)
+		.where(
+			(bom_item.bom_no.isnotnull())
+			& (bom_item.bom_no != "")
+			& (bom.docstatus == 1)
+			& (bom.is_active == 1)
+			& (bom_item.parenttype == "BOM")
+		)
+	).run(as_dict=True)
+
+	child_parent_map = defaultdict(list)
+	parent_child_map = defaultdict(list)
+	for row in bom_items:
+		child_parent_map[row.bom_no].append(row.parent)
+		parent_child_map[row.parent].append(row.bom_no)
+
+	return child_parent_map, parent_child_map
+
+
+def set_values_in_log(log_name: str, values: Dict[str, Any], commit: bool = False) -> None:
+	"Update BOM Update Log record."
+
+	if not values:
+		return
+
+	bom_update_log = frappe.qb.DocType("BOM Update Log")
+	query = frappe.qb.update(bom_update_log).where(bom_update_log.name == log_name)
+
+	for key, value in values.items():
+		query = query.set(key, value)
+	query.run()
+
+	if commit and not frappe.flags.in_test:
+		frappe.db.commit()  # nosemgrep
+
+
+def handle_exception(doc: "BOMUpdateLog") -> None:
+	"Rolls back and fails BOM Update Log."
+
+	frappe.db.rollback()
+	error_log = doc.log_error("BOM Update Tool Error")
+	set_values_in_log(doc.name, {"status": "Failed", "error_log": error_log.name})
diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
index 47efea9..b38fc89 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
@@ -6,9 +6,12 @@
 
 from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import (
 	BOMMissingError,
-	run_bom_job,
+	resume_bom_cost_update_jobs,
 )
-from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom
+from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import (
+	enqueue_replace_bom,
+	enqueue_update_cost,
+)
 
 test_records = frappe.get_test_records("BOM")
 
@@ -31,17 +34,12 @@
 	def tearDown(self):
 		frappe.db.rollback()
 
-		if self._testMethodName == "test_bom_update_log_completion":
-			# clear logs and delete BOM created via setUp
-			frappe.db.delete("BOM Update Log")
-			self.new_bom_doc.cancel()
-			self.new_bom_doc.delete()
-
-			# explicitly commit and restore to original state
-			frappe.db.commit()  # nosemgrep
-
 	def test_bom_update_log_validate(self):
-		"Test if BOM presence is validated."
+		"""
+		1) Test if BOM presence is validated.
+		2) Test if same BOMs are validated.
+		3) Test of non-existent BOM is validated.
+		"""
 
 		with self.assertRaises(BOMMissingError):
 			enqueue_replace_bom(boms={})
@@ -52,45 +50,22 @@
 		with self.assertRaises(frappe.ValidationError):
 			enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom="Dummy BOM"))
 
-	def test_bom_update_log_queueing(self):
-		"Test if BOM Update Log is created and queued."
-
-		log = enqueue_replace_bom(
-			boms=self.boms,
-		)
-
-		self.assertEqual(log.docstatus, 1)
-		self.assertEqual(log.status, "Queued")
-
 	def test_bom_update_log_completion(self):
 		"Test if BOM Update Log handles job completion correctly."
 
-		log = enqueue_replace_bom(
-			boms=self.boms,
-		)
-
-		# Explicitly commits log, new bom (setUp) and replacement impact.
-		# Is run via background jobs IRL
-		run_bom_job(
-			doc=log,
-			boms=self.boms,
-			update_type="Replace BOM",
-		)
+		log = enqueue_replace_bom(boms=self.boms)
 		log.reload()
-
 		self.assertEqual(log.status, "Completed")
 
-		# teardown (undo replace impact) due to commit
-		boms = frappe._dict(
-			current_bom=self.boms.new_bom,
-			new_bom=self.boms.current_bom,
-		)
-		log2 = enqueue_replace_bom(
-			boms=self.boms,
-		)
-		run_bom_job(  # Explicitly commits
-			doc=log2,
-			boms=boms,
-			update_type="Replace BOM",
-		)
-		self.assertEqual(log2.status, "Completed")
+
+def update_cost_in_all_boms_in_test():
+	"""
+	Utility to run 'Update Cost' job in tests without Cron job until fully complete.
+	"""
+	log = enqueue_update_cost()  # create BOM Update Log
+
+	while log.status != "Completed":
+		resume_bom_cost_update_jobs()  # run cron job until complete
+		log.reload()
+
+	return log
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
index b0e7da1..d16fcd0 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
@@ -10,8 +10,6 @@
 import frappe
 from frappe.model.document import Document
 
-from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order
-
 
 class BOMUpdateTool(Document):
 	pass
@@ -40,14 +38,13 @@
 def auto_update_latest_price_in_all_boms() -> None:
 	"""Called via hooks.py."""
 	if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
-		update_cost()
-
-
-def update_cost() -> None:
-	"""Updates Cost for all BOMs from bottom to top."""
-	bom_list = get_boms_in_bottom_up_order()
-	for bom in bom_list:
-		frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True)
+		wip_log = frappe.get_all(
+			"BOM Update Log",
+			{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
+			limit_page_length=1,
+		)
+		if not wip_log:
+			create_bom_update_log(update_type="Update Cost")
 
 
 def create_bom_update_log(
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
index fae72a0..5dd557f 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
@@ -1,11 +1,13 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
 import frappe
 from frappe.tests.utils import FrappeTestCase
 
-from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom
-from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
+from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
+	update_cost_in_all_boms_in_test,
+)
+from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom
 from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
 from erpnext.stock.doctype.item.test_item import create_item
 
@@ -15,6 +17,9 @@
 class TestBOMUpdateTool(FrappeTestCase):
 	"Test major functions run via BOM Update Tool."
 
+	def tearDown(self):
+		frappe.db.rollback()
+
 	def test_replace_bom(self):
 		current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
 
@@ -23,15 +28,10 @@
 		bom_doc.insert()
 
 		boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name)
-		replace_bom(boms)
+		enqueue_replace_bom(boms=boms)
 
-		self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom))
-		self.assertTrue(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", bom_doc.name))
-
-		# reverse, as it affects other testcases
-		boms.current_bom = bom_doc.name
-		boms.new_bom = current_bom
-		replace_bom(boms)
+		self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1}))
+		self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1}))
 
 	def test_bom_cost(self):
 		for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
@@ -52,13 +52,13 @@
 		self.assertEqual(doc.total_cost, 200)
 
 		frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 200)
-		update_cost()
+		update_cost_in_all_boms_in_test()
 
 		doc.load_from_db()
 		self.assertEqual(doc.total_cost, 300)
 
 		frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 100)
-		update_cost()
+		update_cost_in_all_boms_in_test()
 
 		doc.load_from_db()
 		self.assertEqual(doc.total_cost, 200)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 891a497..e88049d 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -798,7 +798,6 @@
 
 	for item in args.raw_materials:
 		item_doc = frappe.get_doc("Item", item)
-
 		bom.append(
 			"items",
 			{
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 2aba482..27e7e24 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -417,7 +417,7 @@
 					"doctype": "Item Price",
 					"item_code": "_Test FG Non Stock Item",
 					"price_list_rate": 1000,
-					"price_list": "Standard Buying",
+					"price_list": "_Test Price List India",
 				}
 			).insert(ignore_permissions=True)
 
@@ -426,8 +426,17 @@
 			item_code="_Test FG Item", target="_Test Warehouse - _TC", qty=1, basic_rate=100
 		)
 
-		if not frappe.db.get_value("BOM", {"item": fg_item}):
-			make_bom(item=fg_item, rate=1000, raw_materials=["_Test FG Item", "_Test FG Non Stock Item"])
+		if not frappe.db.get_value("BOM", {"item": fg_item, "docstatus": 1}):
+			bom = make_bom(
+				item=fg_item,
+				rate=1000,
+				raw_materials=["_Test FG Item", "_Test FG Non Stock Item"],
+				do_not_save=True,
+			)
+			bom.rm_cost_as_per = "Price List"  # non stock item won't have valuation rate
+			bom.buying_price_list = "_Test Price List India"
+			bom.currency = "INR"
+			bom.save()
 
 		wo = make_wo_order_test_record(production_item=fg_item)
 
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index 91fccfa..090697b 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -244,11 +244,10 @@
 			)
 
 			for d in item_details:
-				if d.item_code not in self.invoice_items.get(d.parent, {}):
-					self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
-					self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
-						"base_net_amount", 0
-					)
+				self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
+				self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
+					"base_net_amount", 0
+				)
 
 				if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
 					self.is_nil_exempt.append(d.item_code)
@@ -335,7 +334,6 @@
 
 	def set_outward_taxable_supplies(self):
 		inter_state_supply_details = {}
-
 		for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
 			gst_category = self.invoice_detail_map.get(inv, {}).get("gst_category")
 			place_of_supply = (
@@ -361,7 +359,6 @@
 							else:
 								self.report_dict["sup_details"]["osup_det"]["iamt"] += taxable_value * rate / 100
 								self.report_dict["sup_details"]["osup_det"]["txval"] += taxable_value
-
 								if (
 									gst_category in ["Unregistered", "Registered Composition", "UIN Holders"]
 									and self.gst_details.get("gst_state") != place_of_supply.split("-")[1]
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 9add09b..5eb14a5 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -55,6 +55,9 @@
 		return False
 
 	invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")})
+	invalid_company_gstin = not frappe.db.get_value(
+		"E Invoice User", {"gstin": doc.get("company_gstin")}
+	)
 	invalid_supply_type = doc.get("gst_category") not in [
 		"Registered Regular",
 		"Registered Composition",
@@ -71,6 +74,7 @@
 
 	if (
 		invalid_company
+		or invalid_company_gstin
 		or invalid_supply_type
 		or company_transaction
 		or no_taxes_applied
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 8889a5f..35e0b0d 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -141,6 +141,9 @@
 				)
 
 	def validate_internal_customer(self):
+		if not self.is_internal_customer:
+			self.represents_company = ""
+
 		internal_customer = frappe.db.get_value(
 			"Customer",
 			{
diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py
index 309658d..cfe3d62 100644
--- a/erpnext/setup/doctype/authorization_control/authorization_control.py
+++ b/erpnext/setup/doctype/authorization_control/authorization_control.py
@@ -135,8 +135,8 @@
 			price_list_rate, base_rate = 0, 0
 			for d in doc_obj.get("items"):
 				if d.base_rate:
-					price_list_rate += flt(d.base_price_list_rate) or flt(d.base_rate)
-					base_rate += flt(d.base_rate)
+					price_list_rate += (flt(d.base_price_list_rate) or flt(d.base_rate)) * flt(d.qty)
+					base_rate += flt(d.base_rate) * flt(d.qty)
 			if doc_obj.get("discount_amount"):
 				base_rate -= flt(doc_obj.discount_amount)
 
diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json
index 1b2dbc6..f02fc45 100644
--- a/erpnext/setup/module_onboarding/home/home.json
+++ b/erpnext/setup/module_onboarding/home/home.json
@@ -25,7 +25,7 @@
  "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup",
  "idx": 0,
  "is_complete": 0,
- "modified": "2021-12-15 14:23:52.460913",
+ "modified": "2022-06-07 14:31:00.575193",
  "modified_by": "Administrator",
  "module": "Setup",
  "name": "Home",
diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json
index 48741dc..4999a36 100644
--- a/erpnext/setup/onboarding_step/data_import/data_import.json
+++ b/erpnext/setup/onboarding_step/data_import/data_import.json
@@ -2,14 +2,14 @@
  "action": "Watch Video",
  "action_label": "Learn more about data migration",
  "creation": "2021-05-19 05:29:16.809610",
- "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc). If you are migrating from [Tally](https://tallysolutions.com/) or [Quickbooks](https://quickbooks.intuit.com/in/), we got special migration tools for you.",
+ "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc).",
  "docstatus": 0,
  "doctype": "Onboarding Step",
  "idx": 0,
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-12-15 13:10:57.346422",
+ "modified": "2022-06-07 14:28:51.390813",
  "modified_by": "Administrator",
  "name": "Data import",
  "owner": "Administrator",
diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json
index 388853d..cf07968 100644
--- a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json
+++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json
@@ -9,7 +9,7 @@
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-12-15 14:20:55.441678",
+ "modified": "2022-06-07 14:28:00.901082",
  "modified_by": "Administrator",
  "name": "Navigation Help",
  "owner": "Administrator",
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 4f3e842..2f6d4fb 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -11,7 +11,7 @@
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
-  "name_and_description_section",
+  "details",
   "naming_series",
   "item_code",
   "variant_of",
@@ -35,11 +35,11 @@
   "over_billing_allowance",
   "image",
   "section_break_11",
-  "brand",
   "description",
-  "sb_barcodes",
-  "barcodes",
+  "brand",
+  "dashboard_tab",
   "inventory_section",
+  "inventory_settings_section",
   "shelf_life_in_days",
   "end_of_life",
   "default_material_request_type",
@@ -49,6 +49,8 @@
   "weight_per_unit",
   "weight_uom",
   "allow_negative_stock",
+  "sb_barcodes",
+  "barcodes",
   "reorder_section",
   "reorder_levels",
   "unit_of_measure_conversion",
@@ -67,13 +69,13 @@
   "has_variants",
   "variant_based_on",
   "attributes",
-  "defaults",
+  "accounting",
   "item_defaults",
-  "purchase_details",
-  "is_purchase_item",
+  "purchasing_tab",
   "purchase_uom",
   "min_order_qty",
   "safety_stock",
+  "is_purchase_item",
   "purchase_details_cb",
   "lead_time_days",
   "last_purchase_rate",
@@ -83,33 +85,31 @@
   "delivered_by_supplier",
   "column_break2",
   "supplier_items",
+  "deferred_expense_section",
+  "enable_deferred_expense",
+  "deferred_expense_account",
+  "no_of_months_exp",
   "foreign_trade_details",
   "country_of_origin",
   "column_break_59",
   "customs_tariff_number",
   "sales_details",
   "sales_uom",
-  "is_sales_item",
   "grant_commission",
+  "is_sales_item",
   "column_break3",
   "max_discount",
   "deferred_revenue",
-  "deferred_revenue_account",
   "enable_deferred_revenue",
-  "column_break_85",
+  "deferred_revenue_account",
   "no_of_months",
-  "deferred_expense_section",
-  "deferred_expense_account",
-  "enable_deferred_expense",
-  "column_break_88",
-  "no_of_months_exp",
   "customer_details",
   "customer_items",
   "item_tax_section_break",
   "taxes",
-  "inspection_criteria",
-  "quality_inspection_template",
+  "quality_tab",
   "inspection_required_before_purchase",
+  "quality_inspection_template",
   "inspection_required_before_delivery",
   "manufacturing",
   "default_bom",
@@ -118,18 +118,11 @@
   "customer_code",
   "default_item_manufacturer",
   "default_manufacturer_part_no",
-  "more_information_section",
   "published_in_website",
   "total_projected_qty"
  ],
  "fields": [
   {
-   "fieldname": "name_and_description_section",
-   "fieldtype": "Section Break",
-   "oldfieldtype": "Section Break",
-   "options": "fa fa-flag"
-  },
-  {
    "fieldname": "naming_series",
    "fieldtype": "Select",
    "label": "Series",
@@ -315,7 +308,7 @@
    "collapsible_depends_on": "is_stock_item",
    "depends_on": "is_stock_item",
    "fieldname": "inventory_section",
-   "fieldtype": "Section Break",
+   "fieldtype": "Tab Break",
    "label": "Inventory",
    "oldfieldtype": "Section Break",
    "options": "fa fa-truck"
@@ -515,30 +508,16 @@
    "options": "Item Variant Attribute"
   },
   {
-   "depends_on": "eval:!doc.is_fixed_asset",
-   "fieldname": "defaults",
-   "fieldtype": "Section Break",
-   "label": "Sales, Purchase, Accounting Defaults"
-  },
-  {
    "fieldname": "item_defaults",
    "fieldtype": "Table",
    "label": "Item Defaults",
    "options": "Item Default"
   },
   {
-   "collapsible": 1,
-   "fieldname": "purchase_details",
-   "fieldtype": "Section Break",
-   "label": "Purchase, Replenishment Details",
-   "oldfieldtype": "Section Break",
-   "options": "fa fa-shopping-cart"
-  },
-  {
    "default": "1",
    "fieldname": "is_purchase_item",
    "fieldtype": "Check",
-   "label": "Is Purchase Item"
+   "label": "Allow Purchase"
   },
   {
    "fieldname": "purchase_uom",
@@ -646,8 +625,8 @@
   {
    "collapsible": 1,
    "fieldname": "sales_details",
-   "fieldtype": "Section Break",
-   "label": "Sales Details",
+   "fieldtype": "Tab Break",
+   "label": "Sales",
    "oldfieldtype": "Section Break",
    "options": "fa fa-tag"
   },
@@ -661,7 +640,7 @@
    "default": "1",
    "fieldname": "is_sales_item",
    "fieldtype": "Check",
-   "label": "Is Sales Item"
+   "label": "Allow Sales"
   },
   {
    "fieldname": "column_break3",
@@ -697,10 +676,6 @@
    "label": "Enable Deferred Revenue"
   },
   {
-   "fieldname": "column_break_85",
-   "fieldtype": "Column Break"
-  },
-  {
    "depends_on": "enable_deferred_revenue",
    "fieldname": "no_of_months",
    "fieldtype": "Int",
@@ -727,10 +702,6 @@
    "label": "Enable Deferred Expense"
   },
   {
-   "fieldname": "column_break_88",
-   "fieldtype": "Column Break"
-  },
-  {
    "depends_on": "enable_deferred_expense",
    "fieldname": "no_of_months_exp",
    "fieldtype": "Int",
@@ -753,8 +724,8 @@
    "collapsible": 1,
    "collapsible_depends_on": "taxes",
    "fieldname": "item_tax_section_break",
-   "fieldtype": "Section Break",
-   "label": "Item Tax",
+   "fieldtype": "Tab Break",
+   "label": "Tax",
    "oldfieldtype": "Section Break",
    "options": "fa fa-money"
   },
@@ -768,15 +739,6 @@
    "options": "Item Tax"
   },
   {
-   "collapsible": 1,
-   "depends_on": "eval:!doc.is_fixed_asset",
-   "fieldname": "inspection_criteria",
-   "fieldtype": "Section Break",
-   "label": "Inspection Criteria",
-   "oldfieldtype": "Section Break",
-   "options": "fa fa-search"
-  },
-  {
    "default": "0",
    "fieldname": "inspection_required_before_purchase",
    "fieldtype": "Check",
@@ -801,7 +763,7 @@
    "collapsible": 1,
    "depends_on": "is_stock_item",
    "fieldname": "manufacturing",
-   "fieldtype": "Section Break",
+   "fieldtype": "Tab Break",
    "label": "Manufacturing",
    "oldfieldtype": "Section Break",
    "options": "fa fa-cogs"
@@ -881,12 +843,6 @@
    "read_only": 1
   },
   {
-   "collapsible": 1,
-   "fieldname": "more_information_section",
-   "fieldtype": "Section Break",
-   "label": "More Information"
-  },
-  {
    "default": "0",
    "depends_on": "published_in_website",
    "fieldname": "published_in_website",
@@ -912,6 +868,40 @@
    "fieldname": "allow_negative_stock",
    "fieldtype": "Check",
    "label": "Allow Negative Stock"
+  },
+  {
+   "fieldname": "inventory_settings_section",
+   "fieldtype": "Section Break",
+   "label": "Inventory Settings"
+  },
+  {
+   "fieldname": "purchasing_tab",
+   "fieldtype": "Tab Break",
+   "label": "Purchasing"
+  },
+  {
+   "fieldname": "quality_tab",
+   "fieldtype": "Tab Break",
+   "label": "Quality"
+  },
+  {
+   "fieldname": "details",
+   "fieldtype": "Tab Break",
+   "label": "Details",
+   "oldfieldtype": "Section Break",
+   "options": "fa fa-flag"
+  },
+  {
+   "fieldname": "dashboard_tab",
+   "fieldtype": "Tab Break",
+   "label": "Dashboard",
+   "show_dashboard": 1
+  },
+  {
+   "depends_on": "eval:!doc.is_fixed_asset",
+   "fieldname": "accounting",
+   "fieldtype": "Tab Break",
+   "label": "Accounting"
   }
  ],
  "icon": "fa fa-tag",
@@ -919,7 +909,7 @@
  "image_field": "image",
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2022-04-28 04:52:10.272256",
+ "modified": "2022-06-08 11:35:20.094546",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Item",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
index c5c0cef..41a3b89 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
@@ -2,11 +2,37 @@
 # See license.txt
 
 
+from typing import TYPE_CHECKING, Optional, overload
+
 import frappe
 from frappe.utils import cint, flt
 
 import erpnext
 
+if TYPE_CHECKING:
+	from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry
+
+
+@overload
+def make_stock_entry(
+	*,
+	item_code: str,
+	qty: float,
+	company: Optional[str] = None,
+	from_warehouse: Optional[str] = None,
+	to_warehouse: Optional[str] = None,
+	rate: Optional[float] = None,
+	serial_no: Optional[str] = None,
+	batch_no: Optional[str] = None,
+	posting_date: Optional[str] = None,
+	posting_time: Optional[str] = None,
+	purpose: Optional[str] = None,
+	do_not_save: bool = False,
+	do_not_submit: bool = False,
+	inspection_required: bool = False,
+) -> "StockEntry":
+	...
+
 
 @frappe.whitelist()
 def make_stock_entry(**args):
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index eb1e0fc..55a213c 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -24,9 +24,10 @@
 	create_stock_reconciliation,
 )
 from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.stock.tests.test_utils import StockTestMixin
 
 
-class TestStockLedgerEntry(FrappeTestCase):
+class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
 	def setUp(self):
 		items = create_items()
 		reset("Stock Entry")
@@ -541,30 +542,6 @@
 				"Incorrect 'Incoming Rate' values fetched for DN items",
 			)
 
-	def assertSLEs(self, doc, expected_sles, sle_filters=None):
-		"""Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
-
-		filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
-		if sle_filters:
-			filters.update(sle_filters)
-		sles = frappe.get_all(
-			"Stock Ledger Entry",
-			fields=["*"],
-			filters=filters,
-			order_by="timestamp(posting_date, posting_time), creation",
-		)
-
-		for exp_sle, act_sle in zip(expected_sles, sles):
-			for k, v in exp_sle.items():
-				act_value = act_sle[k]
-				if k == "stock_queue":
-					act_value = json.loads(act_value)
-					if act_value and act_value[0][0] == 0:
-						# ignore empty fifo bins
-						continue
-
-				self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
-
 	def test_batchwise_item_valuation_stock_reco(self):
 		item, warehouses, batches = setup_item_valuation_test()
 		state = {"stock_value": 0.0, "qty": 0.0}
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 9088eb8..191c03f 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -10,7 +10,7 @@
 from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
 
 from erpnext.accounts.utils import get_stock_and_account_balance
-from erpnext.stock.doctype.item.test_item import create_item, make_item
+from erpnext.stock.doctype.item.test_item import create_item
 from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
@@ -19,10 +19,11 @@
 )
 from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
 from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
+from erpnext.stock.tests.test_utils import StockTestMixin
 from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
 
 
-class TestStockReconciliation(FrappeTestCase):
+class TestStockReconciliation(FrappeTestCase, StockTestMixin):
 	@classmethod
 	def setUpClass(cls):
 		create_batch_or_serial_no_items()
@@ -40,7 +41,7 @@
 		self._test_reco_sle_gle("Moving Average")
 
 	def _test_reco_sle_gle(self, valuation_method):
-		item_code = make_item(properties={"valuation_method": valuation_method}).name
+		item_code = self.make_item(properties={"valuation_method": valuation_method}).name
 
 		se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1", item_code=item_code)
 		company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
@@ -392,7 +393,7 @@
 		SR4		| Reco	|	0	|	6	(posting date: today-1) [backdated]
 		PR3		| PR	|	1	|	7	(posting date: today) # can't post future PR
 		"""
-		item_code = make_item().name
+		item_code = self.make_item().name
 		warehouse = "_Test Warehouse - _TC"
 
 		frappe.flags.dont_execute_stock_reposts = True
@@ -458,7 +459,7 @@
 		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
 		from erpnext.stock.stock_ledger import NegativeStockError
 
-		item_code = make_item().name
+		item_code = self.make_item().name
 		warehouse = "_Test Warehouse - _TC"
 
 		pr1 = make_purchase_receipt(
@@ -506,7 +507,7 @@
 		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
 		from erpnext.stock.stock_ledger import NegativeStockError
 
-		item_code = make_item().name
+		item_code = self.make_item().name
 		warehouse = "_Test Warehouse - _TC"
 
 		sr = create_stock_reconciliation(
@@ -549,7 +550,7 @@
 		# repost will make this test useless, qty should update in realtime without reposts
 		frappe.flags.dont_execute_stock_reposts = True
 
-		item_code = make_item().name
+		item_code = self.make_item().name
 		warehouse = "_Test Warehouse - _TC"
 
 		sr = create_stock_reconciliation(
diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py
index 9ee0c9f..b046dbd 100644
--- a/erpnext/stock/tests/test_utils.py
+++ b/erpnext/stock/tests/test_utils.py
@@ -1,16 +1,67 @@
+import json
+
 import frappe
 from frappe.tests.utils import FrappeTestCase
 
-from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.utils import scan_barcode
 
 
-class TestStockUtilities(FrappeTestCase):
+class StockTestMixin:
+	"""Mixin to simplfy stock ledger tests, useful for all stock transactions."""
+
+	def make_item(self, item_code=None, properties=None, *args, **kwargs):
+		from erpnext.stock.doctype.item.test_item import make_item
+
+		return make_item(item_code, properties, *args, **kwargs)
+
+	def assertSLEs(self, doc, expected_sles, sle_filters=None):
+		"""Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
+
+		filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
+		if sle_filters:
+			filters.update(sle_filters)
+		sles = frappe.get_all(
+			"Stock Ledger Entry",
+			fields=["*"],
+			filters=filters,
+			order_by="timestamp(posting_date, posting_time), creation",
+		)
+
+		for exp_sle, act_sle in zip(expected_sles, sles):
+			for k, v in exp_sle.items():
+				act_value = act_sle[k]
+				if k == "stock_queue":
+					act_value = json.loads(act_value)
+					if act_value and act_value[0][0] == 0:
+						# ignore empty fifo bins
+						continue
+
+				self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
+
+	def assertGLEs(self, doc, expected_gles, gle_filters=None, order_by=None):
+		filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
+
+		if gle_filters:
+			filters.update(gle_filters)
+		actual_gles = frappe.get_all(
+			"GL Entry",
+			fields=["*"],
+			filters=filters,
+			order_by=order_by or "posting_date, creation",
+		)
+
+		for exp_gle, act_gle in zip(expected_gles, actual_gles):
+			for k, exp_value in exp_gle.items():
+				act_value = act_gle[k]
+				self.assertEqual(exp_value, act_value, msg=f"{k} doesn't match \n{exp_gle}\n{act_gle}")
+
+
+class TestStockUtilities(FrappeTestCase, StockTestMixin):
 	def test_barcode_scanning(self):
-		simple_item = make_item(properties={"barcodes": [{"barcode": "12399"}]})
+		simple_item = self.make_item(properties={"barcodes": [{"barcode": "12399"}]})
 		self.assertEqual(scan_barcode("12399")["item_code"], simple_item.name)
 
-		batch_item = make_item(properties={"has_batch_no": 1, "create_new_batch": 1})
+		batch_item = self.make_item(properties={"has_batch_no": 1, "create_new_batch": 1})
 		batch = frappe.get_doc(doctype="Batch", item=batch_item.name).insert()
 
 		batch_scan = scan_barcode(batch.name)
@@ -19,7 +70,7 @@
 		self.assertEqual(batch_scan["has_batch_no"], 1)
 		self.assertEqual(batch_scan["has_serial_no"], 0)
 
-		serial_item = make_item(properties={"has_serial_no": 1})
+		serial_item = self.make_item(properties={"has_serial_no": 1})
 		serial = frappe.get_doc(
 			doctype="Serial No", item_code=serial_item.name, serial_no=frappe.generate_hash()
 		).insert()
diff --git a/erpnext/translations/ar.csv b/erpnext/translations/ar.csv
index 91a9da9..e62f61a 100644
--- a/erpnext/translations/ar.csv
+++ b/erpnext/translations/ar.csv
@@ -4297,7 +4297,7 @@
 "To allow different rates, disable the {0} checkbox in {1}.",للسماح بمعدلات مختلفة ، قم بتعطيل مربع الاختيار {0} في {1}.,
 Current Odometer Value should be greater than Last Odometer Value {0},يجب أن تكون قيمة عداد المسافات الحالية أكبر من قيمة آخر عداد المسافات {0},
 No additional expenses has been added,لم يتم إضافة مصاريف إضافية,
-Asset{} {assets_link} created for {},الأصل {} {asset_link} الذي تم إنشاؤه لـ {},
+Asset{} {assets_link} created for {},الأصل {} {assets_link} الذي تم إنشاؤه لـ {},
 Row {}: Asset Naming Series is mandatory for the auto creation for item {},الصف {}: سلسلة تسمية الأصول إلزامية للإنشاء التلقائي للعنصر {},
 Assets not created for {0}. You will have to create asset manually.,لم يتم إنشاء الأصول لـ {0}. سيكون عليك إنشاء الأصل يدويًا.,
 {0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} يحتوي {1} على إدخالات محاسبية بالعملة {2} للشركة {3}. الرجاء تحديد حساب مستحق أو دائن بالعملة {2}.,
diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv
index 6b766e7..743b294 100644
--- a/erpnext/translations/ru.csv
+++ b/erpnext/translations/ru.csv
@@ -288,7 +288,7 @@
 Assets,Активы,
 Assign,Назначить,
 Assign Salary Structure,Назначить структуру заработной платы,
-Assign To,Назначить в,
+Assign To,Назначить для,
 Assign to Employees,Назначить сотрудникам,
 Assigning Structures...,Назначение структур...,
 Associate,Помощник,
@@ -421,7 +421,7 @@
 Bundle items at time of sale.,Собирать продукты в момент продажи.,
 Business Development Manager,Менеджер по развитию бизнеса,
 Buy,Купить,
-Buying,Покупки,
+Buying,Закупки,
 Buying Amount,Сумма покупки,
 Buying Price List,Ценовой список покупок,
 Buying Rate,Частота покупки,
@@ -490,7 +490,7 @@
 Capital Stock,Капитал,
 Capital Work in Progress,Капитальная работа в процессе,
 Cart,Корзина,
-Cart is Empty,Корзина Пусто,
+Cart is Empty,Корзина пуста,
 Case No(s) already in use. Try from Case No {0},Случай Нет (ы) уже используется. Попробуйте из дела № {0},
 Cash,Наличные,
 Cash Flow Statement,О движении денежных средств,
@@ -578,7 +578,7 @@
 Compensatory leave request days not in valid holidays,Дни запроса на получение компенсационных отчислений не действительны,
 Complaint,Жалоба,
 Completion Date,Дата завершения,
-Computer,компьютер,
+Computer,Компьютер,
 Condition,Условия,
 Configure,Конфигурировать,
 Configure {0},Настроить {0},
@@ -643,7 +643,6 @@
 Course Enrollment {0} does not exists,Зачисление на курс {0} не существует,
 Course Schedule,Расписание курса,
 Course: ,Курс: ,
-Cr,Cr,
 Create,Создать,
 Create BOM,Создать спецификацию,
 Create Delivery Trip,Создать маршрут доставки,
@@ -795,7 +794,6 @@
 Define Project type.,Установите тип проекта.,
 Define budget for a financial year.,Определить бюджет на финансовый год.,
 Define various loan types,Определение различных видов кредита,
-Del,Del,
 Delay in payment (Days),Задержка в оплате (дни),
 Delete all the Transactions for this Company,Удалить все транзакции этой компании,
 Deletion is not permitted for country {0},Для страны не разрешено удаление {0},
@@ -1287,12 +1285,12 @@
 Institute Abbreviation,институт Аббревиатура,
 Institute Name,Название института,
 Instructor,Инструктор,
-Insufficient Stock,Недостаточный Stock,
-Insurance Start date should be less than Insurance End date,"Дата страхование начала должна быть меньше, чем дата страхование End",
+Insufficient Stock,Недостаточный запас,
+Insurance Start date should be less than Insurance End date,"Дата начала страхования должна быть раньше, чем дата окончания",
 Integrated Tax,Интегрированный налог,
 Inter-State Supplies,Межгосударственные поставки,
-Interest Amount,Проценты Сумма,
-Interests,интересы,
+Interest Amount,Сумма процентов,
+Interests,Интересы,
 Intern,Стажер,
 Internet Publishing,Интернет издания,
 Intra-State Supplies,Внутригосударственные поставки,
@@ -1397,7 +1395,7 @@
 Job Description,Описание работы,
 Job Offer,Предложение работы,
 Job card {0} created,Карта работы {0} создана,
-Jobs,Работы,
+Jobs,Вакансии,
 Join,Присоединиться,
 Journal Entries {0} are un-linked,Записи в журнале {0} не-связаны,
 Journal Entry,Запись в журнале,
@@ -1925,7 +1923,7 @@
 Pending Leaves,Ожидающие листья,
 Pending Qty,В ожидании кол-во,
 Pending Quantity,Количество в ожидании,
-Pending Review,В ожидании отзыв,
+Pending Review,В ожидании отзыва,
 Pending activities for today,В ожидании деятельность на сегодняшний день,
 Pension Funds,Пенсионные фонды,
 Percentage Allocation should be equal to 100%,Процент Распределение должно быть равно 100%,
@@ -1949,7 +1947,7 @@
 Planning,Планирование,
 Plants and Machineries,Растения и Механизмов,
 Please Set Supplier Group in Buying Settings.,Установите группу поставщиков в разделе «Настройки покупок».,
-Please add a Temporary Opening account in Chart of Accounts,"Пожалуйста, добавьте временный вступительный счет в План счетов",
+Please add a Temporary Opening account in Chart of Accounts,"Пожалуйста, добавьте временный вступительный счет в план счетов",
 Please add the account to root level Company - ,"Пожалуйста, добавьте счет на корневой уровень компании -",
 Please add the remaining benefits {0} to any of the existing component,Добавьте оставшиеся преимущества {0} к любому из существующих компонентов,
 Please check Multi Currency option to allow accounts with other currency,"Пожалуйста, проверьте мультивалютный вариант, позволяющий счета другой валюте",
@@ -2146,7 +2144,7 @@
 Previous Financial Year is not closed,Предыдущий финансовый год не закрыт,
 Price,Цена,
 Price List,Прайс-лист,
-Price List Currency not selected,Прайс-лист Обмен не выбран,
+Price List Currency not selected,Валюта прайс-листа не выбрана,
 Price List Rate,Прайс-лист Оценить,
 Price List master.,Мастер Прайс-лист.,
 Price List must be applicable for Buying or Selling,Прайс-лист должен быть применим для покупки или продажи,
@@ -2347,7 +2345,7 @@
 Remaining Balance,Остаток средств,
 Remarks,Примечания,
 Reminder to update GSTIN Sent,Напоминание об обновлении отправленного GSTIN,
-Remove item if charges is not applicable to that item,"Удалить продукт, если сборы не применимы к этому продукту",
+Remove item if charges is not applicable to that item,"Удалить объект, если к нему не применяются сборы",
 Removed items with no change in quantity or value.,Удалены пункты без изменения в количестве или стоимости.,
 Reopen,Возобновить,
 Reorder Level,Уровень переупорядочения,
@@ -2509,7 +2507,7 @@
 Salary Slip of employee {0} already created for time sheet {1},Зарплата Скольжение работника {0} уже создан для табеля {1},
 Salary Slip submitted for period from {0} to {1},"Зарплатная ведомость отправлена за период с {0} по {1}",
 Salary Structure Assignment for Employee already exists,Присвоение структуры зарплаты сотруднику уже существует,
-Salary Structure Missing,Структура заработной платы Отсутствующий,
+Salary Structure Missing,Структура заработной платы отсутствует,
 Salary Structure must be submitted before submission of Tax Ememption Declaration,Структура заработной платы должна быть представлена до подачи декларации об освобождении от налогов,
 Salary Structure not found for employee {0} and date {1},Структура зарплаты не найдена для сотрудника {0} и даты {1},
 Salary Structure should have flexible benefit component(s) to dispense benefit amount,Структура заработной платы должна иметь гибкий компонент (ы) выгоды для распределения суммы пособия,
@@ -2701,10 +2699,10 @@
 Setup mode of POS (Online / Offline),Режим настройки POS (Online / Offline),
 Setup your Institute in ERPNext,Установите свой институт в ERPNext,
 Share Balance,Баланс акций,
-Share Ledger,Поделиться записями,
+Share Ledger,Записи по акциям,
 Share Management,Управление долями,
 Share Transfer,Передача акций,
-Share Type,Share Тип,
+Share Type,Тип акций,
 Shareholder,Акционер,
 Ship To State,Корабль в штат,
 Shipments,Поставки,
@@ -2796,8 +2794,8 @@
 Stock Expenses,Расходы по Запасам,
 Stock In Hand,Запасы на руках,
 Stock Items,Позиции на складе,
-Stock Ledger,Книга учета Запасов,
-Stock Ledger Entries and GL Entries are reposted for the selected Purchase Receipts,Записи складской книги и записи GL запасов отправляются для выбранных покупок,
+Stock Ledger,Книга учета запасов,
+Stock Ledger Entries and GL Entries are reposted for the selected Purchase Receipts,Записи книги учета запасов и записи GL повторно публикуются для выбранных квитанций о покупках,
 Stock Levels,Уровень запасов,
 Stock Liabilities,Обязательства по запасам,
 Stock Options,Опционы,
@@ -2829,9 +2827,9 @@
 Student Group,Учебная группа,
 Student Group Strength,Сила студенческой группы,
 Student Group is already updated.,Студенческая группа уже обновлена.,
-Student Group: ,Студенческая группа:,
+Student Group: ,Студенческая группа: ,
 Student ID,Студенческий билет,
-Student ID: ,Студенческий билет:,
+Student ID: ,Студенческий билет: ,
 Student LMS Activity,Студенческая LMS Активность,
 Student Mobile No.,Мобильный номер студента,
 Student Name,Имя ученика,
@@ -2864,9 +2862,9 @@
 Successfully deleted all transactions related to this company!,"Успешно удален все сделки, связанные с этой компанией!",
 Sum of Scores of Assessment Criteria needs to be {0}.,Сумма десятков критериев оценки должно быть {0}.,
 Sum of points for all goals should be 100. It is {0},Сумма баллов за все цели должны быть 100. Это {0},
-Summary,Резюме,
-Summary for this month and pending activities,Резюме для этого месяца и в ожидании деятельности,
-Summary for this week and pending activities,Резюме на этой неделе и в ожидании деятельности,
+Summary,Сводка,
+Summary for this month and pending activities,Сводка за этот месяц и предстоящие мероприятия,
+Summary for this week and pending activities,Сводка за эту неделю и предстоящие мероприятия,
 Sunday,Воскресенье,
 Suplier,Поставщик,
 Supplier,Поставщик,
@@ -2880,7 +2878,7 @@
 Supplier Part No,Деталь поставщика №,
 Supplier Quotation,Предложение поставщика,
 Supplier Scorecard,Оценочная карта поставщика,
-Supplier Warehouse mandatory for sub-contracted Purchase Receipt,Поставщик Склад обязательным для субподрядчиком ТОВАРНЫЙ ЧЕК,
+Supplier Warehouse mandatory for sub-contracted Purchase Receipt,Наличие склада поставщика обязательно для субподрядной квитанции о покупке,
 Supplier database.,База данных поставщиков.,
 Supplier {0} not found in {1},Поставщик {0} не найден в {1},
 Supplier(s),Поставщик(и),
@@ -3199,7 +3197,7 @@
 User,Пользователь,
 User ID,ID пользователя,
 User ID not set for Employee {0},ID пользователя не установлен для сотрудника {0},
-User Remark,Примечание Пользователь,
+User Remark,Примечание пользователя,
 User has not applied rule on the invoice {0},Пользователь не применил правило к счету {0},
 User {0} already exists,Пользователь {0} уже существует,
 User {0} created,Пользователь {0} создан,
@@ -3243,7 +3241,7 @@
 View Form,Посмотреть форму,
 View Lab Tests,Просмотр лабораторных тестов,
 View Leads,Посмотреть лиды,
-View Ledger,Посмотреть Леджер,
+View Ledger,Посмотреть записи,
 View Now,Просмотр сейчас,
 View a list of all the help videos,Просмотреть список всех справочных видео,
 View in Cart,Смотрите в корзину,
@@ -3314,7 +3312,7 @@
 Work Summary for {0},Резюме работы для {0},
 Work-in-Progress Warehouse is required before Submit,Работа-в-Прогресс Склад требуется перед Отправить,
 Workflow,Рабочий процесс,
-Working,Работающий,
+Working,В работе,
 Working Hours,Часы работы,
 Workstation,Рабочее место,
 Workstation is closed on the following dates as per Holiday List: {0},Рабочая место закрыто в следующие даты согласно списка праздников: {0},
@@ -3869,13 +3867,17 @@
 Not Allowed,Не разрешено,
 Not allowed to create accounting dimension for {0},Не разрешено создавать учетное измерение для {0},
 Not permitted. Please disable the Lab Test Template,"Не разрешено Пожалуйста, отключите шаблон лабораторного теста",
-Note,Заметки,
+Note,Заметка,
 Notes: ,Заметки: ,
-On Converting Opportunity,О возможности конвертации,
-On Purchase Order Submission,При подаче заказа на поставку,
-On Sales Order Submission,На подаче заказа клиента,
-On Task Completion,По завершении задачи,
+On Converting Opportunity,Конвертацию возможности,
+On Purchase Order Submission,Офомление заказа на закупку,
+On Sales Order Submission,Оформление заказа на продажу,
+On Task Completion,Завершении задачи,
 On {0} Creation,На {0} создании,
+On Item Creation,Создание продукта,
+On Lead Creation,Создание лида,
+On Supplier Creation,Создание поставщика,
+On Customer Creation,Создание клиента,
 Only .csv and .xlsx files are supported currently,В настоящее время поддерживаются только файлы .csv и .xlsx,
 Only expired allocation can be cancelled,Только истекшее распределение может быть отменено,
 Only users with the {0} role can create backdated leave applications,Только пользователи с ролью {0} могут создавать оставленные приложения с задним сроком действия,
@@ -4217,7 +4219,7 @@
 No students Found,Студенты не найдены,
 Not in Stock,Нет в наличии,
 Please select a Customer,Выберите клиента,
-Printed On,Отпечатано на,
+Printed On,Напечатано на,
 Received From,Получено от,
 Sales Person,Продавец,
 To date cannot be before From date,На сегодняшний день не может быть раньше От даты,
@@ -4945,14 +4947,14 @@
 Min Amt,Мин Amt,
 Max Amt,Макс Амт,
 Period Settings,Настройки периода,
-Margin,Разница,
+Margin,Маржа,
 Margin Type,Тип маржа,
 Margin Rate or Amount,Маржинальная ставка или сумма,
 Price Discount Scheme,Схема скидок,
 Rate or Discount,Стоимость или скидка,
 Discount Percentage,Скидка в процентах,
 Discount Amount,Сумма скидки,
-For Price List,Для Прейскурантом,
+For Price List,Для прайс-листа,
 Product Discount Scheme,Схема скидок на товары,
 Same Item,Тот же пункт,
 Free Item,Бесплатный товар,
@@ -5385,18 +5387,18 @@
 Insurance End Date,Дата окончания страхования,
 Comprehensive Insurance,Комплексное страхование,
 Maintenance Required,Требуется техническое обслуживание,
-Check if Asset requires Preventive Maintenance or Calibration,"Проверьте, требуется ли Asset профилактическое обслуживание или калибровка",
+Check if Asset requires Preventive Maintenance or Calibration,"Проверьте, требует ли актив профилактического обслуживания или калибровки",
 Booked Fixed Asset,Забронированные основные средства,
 Purchase Receipt Amount,Сумма покупки,
 Default Finance Book,Финансовая книга по умолчанию,
 Quality Manager,Менеджер по качеству,
-Asset Category Name,Asset Категория Название,
+Asset Category Name,Название категории активов,
 Depreciation Options,Варианты амортизации,
 Enable Capital Work in Progress Accounting,Включить капитальную работу в процессе учета,
 Finance Book Detail,Финансовая книга,
 Asset Category Account,Счет категории активов,
 Fixed Asset Account,Счет учета основных средств,
-Accumulated Depreciation Account,Начисленной амортизации Счет,
+Accumulated Depreciation Account,Счет накопленной амортизации,
 Depreciation Expense Account,Износ счет расходов,
 Capital Work In Progress Account,Счет капитальной работы,
 Asset Finance Book,Финансовая книга по активам,
@@ -5441,7 +5443,7 @@
 Assign To Name,Назначить имя,
 Repair Status,Статус ремонта,
 Error Description,Описание ошибки,
-Downtime,время простоя,
+Downtime,Время простоя,
 Repair Cost,Стоимость ремонта,
 Manufacturing Manager,Менеджер производства,
 Current Asset Value,Текущая стоимость актива,
@@ -6073,7 +6075,7 @@
 ERPNext Account,Учетная запись ERPNext,
 Shopify Webhook Detail,Узнайте подробности веб-камеры,
 Webhook ID,Идентификатор Webhook,
-Tally Migration,Tally Migration,
+Tally Migration,Tally миграция,
 Master Data,Основные данные,
 "Data exported from Tally that consists of the Chart of Accounts, Customers, Suppliers, Addresses, Items and UOMs","Данные, экспортированные из Tally, которые состоят из плана счетов, клиентов, поставщиков, адресов, позиций и единиц измерения",
 Is Master Data Processed,Обработка основных данных,
@@ -6082,7 +6084,7 @@
 Creditors Account set in Tally,Счет кредиторов установлен в Tally,
 Tally Debtors Account,Счет Tally должников,
 Debtors Account set in Tally,Счет дебитора установлен в Tally,
-Tally Company,Талли Компания,
+Tally Company,Tally Компания,
 Company Name as per Imported Tally Data,Название компании согласно импортированным данным подсчета,
 Default UOM,Единица измерения по умолчанию,
 UOM in case unspecified in imported data,"Единицы измерения, если они не указаны в импортированных данных",
@@ -6108,7 +6110,7 @@
 Creation User,Создание пользователя,
 "The user that will be used to create Customers, Items and Sales Orders. This user should have the relevant permissions.","Пользователь, который будет использоваться для создания клиентов, товаров и заказов на продажу. Этот пользователь должен иметь соответствующие разрешения.",
 "This warehouse will be used to create Sales Orders. The fallback warehouse is ""Stores"".",Этот склад будет использоваться для создания заказов на продажу. Резервный склад &quot;Магазины&quot;.,
-"The fallback series is ""SO-WOO-"".",Аварийная серия &quot;SO-WOO-&quot;.,
+"The fallback series is ""SO-WOO-"".","Аварийная серия ""SO-WOO-"".",
 This company will be used to create Sales Orders.,Эта компания будет использоваться для создания заказов на продажу.,
 Delivery After (Days),Доставка после (дней),
 This is the default offset (days) for the Delivery Date in Sales Orders. The fallback offset is 7 days from the order placement date.,Это смещение по умолчанию (дни) для даты поставки в заказах на продажу. Смещение отступления составляет 7 дней с даты размещения заказа.,
@@ -6441,7 +6443,7 @@
 Applicant Name,Имя заявителя,
 Appointment Date,Назначенная дата,
 Appointment Letter Template,Шаблон письма о назначении,
-Body,Тело,
+Body,Содержимое,
 Closing Notes,Заметки,
 Appointment Letter content,Письмо о назначении,
 Appraisal,Оценка,
@@ -6455,7 +6457,7 @@
 Key Responsibility Area,Основная зона ответственности,
 Weightage (%),Весовая нагрузка (%),
 Score (0-5),Оценка (0-5),
-Score Earned,Оценка Заработано,
+Score Earned,Оценка получена,
 Appraisal Template Title,Название шаблона оценки,
 Appraisal Template Goal,Цель шаблона оценки,
 KRA,КРА,
@@ -6747,7 +6749,7 @@
 Awaiting Response,В ожидании ответа,
 Job Offer Terms,Условия работы,
 Select Terms and Conditions,Выберите Сроки и условия,
-Printing Details,Печатать Подробности,
+Printing Details,Подробности печати,
 Job Offer Term,Срок действия предложения,
 Offer Term,Условие предложения,
 Value / Description,Значение / Описание,
@@ -7520,7 +7522,7 @@
 Is Milestone,Является этапом,
 Task Description,Описание задания,
 Dependencies,Зависимости,
-Dependent Tasks,Зависимые задачи,
+Dependent Tasks,Зависит от задач,
 Depends on Tasks,Зависит от задач,
 Actual Start Date (via Time Sheet),Фактическая дата начала (по табелю учета рабочего времени),
 Actual Time (in hours),Фактическое время (в часах),
@@ -7645,7 +7647,7 @@
 Buyer of Goods and Services.,Покупатель товаров и услуг.,
 CUST-.YYYY.-,CUST-.YYYY.-,
 Default Company Bank Account,Стандартный банковский счет компании,
-From Lead,Из Лида,
+From Lead,Из лида,
 Account Manager,Менеджер по работе с клиентами,
 Allow Sales Invoice Creation Without Sales Order,Разрешить создание счета без заказа на продажу,
 Allow Sales Invoice Creation Without Delivery Note,Разрешить создание счета без накладной,
@@ -7818,14 +7820,14 @@
 Company Description,Описание компании,
 Registration Details,Регистрационные данные,
 Company registration numbers for your reference. Tax numbers etc.,Регистрационные номера компании для вашей справки. Налоговые числа и т.д.,
-Delete Company Transactions,Удалить Сделки Компания,
+Delete Company Transactions,Удалить транзакции компании,
 Currency Exchange,Курс обмена валюты,
 Specify Exchange Rate to convert one currency into another,Укажите Курс конвертировать одну валюту в другую,
 From Currency,Из валюты,
 To Currency,В валюту,
 For Buying,Для покупки,
 For Selling,Для продажи,
-Customer Group Name,Группа Имя клиента,
+Customer Group Name,Название группы клиентов,
 Parent Customer Group,Родительская группа клиента,
 Only leaf nodes are allowed in transaction,Только листовые узлы допускаются в сделке,
 Mention if non-standard receivable account applicable,Упоминание если нестандартная задолженность счет применимо,
@@ -7893,7 +7895,7 @@
 Update Series Number,Обновить Идентификаторы по Номеру,
 Quotation Lost Reason,Причина Отказа от Предложения,
 A third party distributor / dealer / commission agent / affiliate / reseller who sells the companies products for a commission.,"Сторонний дистрибьютер, дилер, агент, филиал или реселлер, который продаёт продукты компании за комиссионное вознаграждение.",
-Sales Partner Name,Имя Партнера по продажам,
+Sales Partner Name,Имя партнера по продажам,
 Partner Type,Тип партнера,
 Address & Contacts,Адрес и контакты,
 Address Desc,Адрес по убыванию,
@@ -7914,7 +7916,7 @@
 Set targets Item Group-wise for this Sales Person.,Задайте цели Продуктовых Групп для Продавца,
 Supplier Group Name,Название группы поставщиков,
 Parent Supplier Group,Родительская группа поставщиков,
-Target Detail,Цель Подробности,
+Target Detail,Подробности цели,
 Target Qty,Целевое количество,
 Target  Amount,Целевая сумма,
 Target Distribution,Распределение цели,
@@ -7973,13 +7975,13 @@
 Issue Credit Note,Кредитная кредитная карта,
 Return Against Delivery Note,Вернуться На накладной,
 Customer's Purchase Order No,Клиентам Заказ Нет,
-Billing Address Name,Адрес для выставления счета Имя,
+Billing Address Name,Название адреса для выставления счета,
 Required only for sample item.,Требуется только для образца пункта.,
 "If you have created a standard template in Sales Taxes and Charges Template, select one and click on the button below.","Если вы создали стандартный шаблон в шаблонах Налоги с налогами и сбором платежей, выберите его и нажмите кнопку ниже.",
 In Words will be visible once you save the Delivery Note.,По словам будет виден только вы сохраните накладной.,
 In Words (Export) will be visible once you save the Delivery Note.,В Слов (Экспорт) будут видны только вы сохраните накладной.,
 Transporter Info,Информация для транспортировки,
-Driver Name,Имя драйвера,
+Driver Name,Имя водителя,
 Track this Delivery Note against any Project,Подписка на Delivery Note против любого проекта,
 Inter Company Reference,Справочник Интер,
 Print Without Amount,Распечатать без суммы,
@@ -8079,7 +8081,7 @@
 Supplier Items,Продукты поставщика,
 Foreign Trade Details,Сведения о внешней торговле,
 Country of Origin,Страна происхождения,
-Sales Details,Продажи Подробности,
+Sales Details,Детали продажи,
 Default Sales Unit of Measure,Единица измерения продаж по умолчанию,
 Is Sales Item,Продаваемый продукт,
 Max Discount (%),Макс. скидка (%),
@@ -8117,7 +8119,7 @@
 Alternative Item Code,Альтернативный код продукта,
 Two-way,Двусторонний,
 Alternative Item Name,Альтернативное название продукта,
-Attribute Name,Имя атрибута,
+Attribute Name,Название атрибута,
 Numeric Values,Числовые значения,
 From Range,От хребта,
 Increment,Приращение,
@@ -8236,8 +8238,8 @@
 Vehicle Number,Номер транспортного средства,
 Vehicle Date,Дата транспортного средства,
 Received and Accepted,Получил и принял,
-Accepted Quantity,Принято Количество,
-Rejected Quantity,Отклонен Количество,
+Accepted Quantity,Количество принятых,
+Rejected Quantity,Количество отклоненных,
 Accepted Qty as per Stock UOM,Принятое количество в соответствии с единицами измерения запаса,
 Sample Quantity,Количество образцов,
 Rate and Amount,Ставку и сумму,
@@ -8285,7 +8287,7 @@
 Warranty Period (Days),Гарантийный срок (дней),
 Serial No Details,Серийный номер подробнее,
 MAT-STE-.YYYY.-,MAT-STE-.YYYY.-,
-Stock Entry Type,Тип входа,
+Stock Entry Type,Тип складской записи,
 Stock Entry (Outward GIT),Вход в акции (внешний GIT),
 Material Consumption for Manufacture,Потребление материала для производства,
 Repack,Перепаковать,
@@ -8447,7 +8449,7 @@
 Sent To,Отправить,
 Absent Student Report,Отчет о пропуске занятия,
 Assessment Plan Status,Статус плана оценки,
-Asset Depreciation Ledger,Износ Леджер активов,
+Asset Depreciation Ledger,Книга амортизации основных средств,
 Asset Depreciations and Balances,Активов Амортизация и противовесов,
 Available Stock for Packing Items,Доступные Запасы для Комплектации Продуктов,
 Bank Clearance Summary,Банк уплата по счетам итого,
@@ -8559,7 +8561,7 @@
 Sales Partner Commission Summary,Сводка комиссий партнеров по продажам,
 Sales Partner Target Variance based on Item Group,Целевое отклонение партнера по продажам на основе группы товаров,
 Sales Partner Transaction Summary,Сводка по сделкам с партнерами по продажам,
-Sales Partners Commission,Комиссионные Партнеров по продажам,
+Sales Partners Commission,Комиссия партнеров по продажам,
 Invoiced Amount (Exclusive Tax),Сумма счета (без учета налога),
 Average Commission Rate,Средний уровень комиссии,
 Sales Payment Summary,Сводка по продажам,
@@ -8579,7 +8581,7 @@
 Student Monthly Attendance Sheet,Ежемесячная посещаемость студентов,
 Subcontracted Item To Be Received,"Субподрядный предмет, подлежащий получению",
 Subcontracted Raw Materials To Be Transferred,Субподрядное сырье для передачи,
-Supplier Ledger Summary,Список поставщиков,
+Supplier Ledger Summary,Сводка книги поставщиков,
 Supplier-Wise Sales Analytics,Аналитика продаж в разрезе поставщиков,
 Support Hour Distribution,Распределение поддержки,
 TDS Computation Summary,Сводка расчетов TDS,
@@ -9242,7 +9244,7 @@
 Tasks Overdue,Просроченные задачи,
 Completion,Завершение,
 Provident Fund Deductions,Отчисления в резервный фонд,
-Purchase Order Analysis,Анализ заказа на закупку,
+Purchase Order Analysis,Анализ заказов на закупку,
 From and To Dates are required.,Укажите даты от и до.,
 To Date cannot be before From Date.,Дата не может быть раньше даты начала.,
 Qty to Bill,Кол-во к счету,
@@ -9267,7 +9269,7 @@
 Amount Delivered,Сумма доставки,
 Delay (in Days),Задержка (в днях),
 Group by Sales Order,Группировать по заказу на продажу,
- Sales Value,Объем продаж,
+ Sales Value, Объем продаж,
 Stock Qty vs Serial No Count,Кол-во на складе по сравнению с серийным номером,
 Serial No Count,Серийный номер,
 Work Order Summary,Сводка заказа на работу,
@@ -9320,8 +9322,8 @@
 A customer is already linked to this Member,Клиент уже привязан к этому участнику,
 End Date must not be lesser than Start Date,Дата окончания не должна быть меньше даты начала.,
 Employee {0} already has Active Shift {1}: {2},Сотрудник {0} уже имеет активную смену {1}: {2},
- from {0},от {0},
- to {0},в {0},
+ from {0}, от {0},
+ to {0}, в {0},
 Please select Employee first.,"Пожалуйста, сначала выберите сотрудника.",
 Please set {0} for the Employee or for Department: {1},Установите {0} для сотрудника или отдела: {1},
 To Date should be greater than From Date,"Дата до должна быть больше, чем Дата",
@@ -9838,3 +9840,8 @@
 Creating Purchase Order ...,Создание заказа на поставку ...,
 "Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Выберите поставщика из списка поставщиков по умолчанию для позиций ниже. При выборе Заказ на поставку будет сделан в отношении товаров, принадлежащих только выбранному Поставщику.",
 Row #{}: You must select {} serial numbers for item {}.,Строка № {}: необходимо выбрать {} серийных номеров для позиции {}.,
+Items & Pricing,Продукты и цены,
+Overdue,Просрочено,
+Completed,Завершенно,
+Total Tasks,Всего задач,
+Build,Конструктор,