Merge pull request #30238 from KrithiRamani/bulk_create_PL_and_DN

feat: Create single PL/DN from several SO. 
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index d05bbbe..afabe43 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -4,7 +4,10 @@
   pull_request:
     paths-ignore:
       - '**.js'
+      - '**.css'
       - '**.md'
+      - '**.html'
+      - '**.csv'
   workflow_dispatch:
 
 concurrency:
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 40f9365..69be765 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -4,8 +4,10 @@
   pull_request:
     paths-ignore:
       - '**.js'
+      - '**.css'
       - '**.md'
       - '**.html'
+      - '**.csv'
   push:
     branches: [ develop ]
     paths-ignore:
diff --git a/.mergify.yml b/.mergify.yml
index b7d1df4..315d90f 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -7,6 +7,8 @@
           - author!=gavindsouza
           - author!=rohitwaghchaure
           - author!=nabinhait
+          - author!=ankush
+          - author!=deepeshgarg007
         - or:
           - base=version-13
           - base=version-12
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 9b3b3aa..91c07ad 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -53,7 +53,7 @@
 
 	def on_submit(self):
 		# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
-		if self.loyalty_program:
+		if not self.is_return and self.loyalty_program:
 			self.make_loyalty_point_entry()
 		elif self.is_return and self.return_against and self.loyalty_program:
 			against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
@@ -87,7 +87,7 @@
 	def on_cancel(self):
 		# run on cancel method of selling controller
 		super(SalesInvoice, self).on_cancel()
-		if self.loyalty_program:
+		if not self.is_return and self.loyalty_program:
 			self.delete_loyalty_point_entry()
 		elif self.is_return and self.return_against and self.loyalty_program:
 			against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 54217fb..3afd72e 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -47,7 +47,6 @@
 	get_serial_nos,
 	update_serial_nos_after_submit,
 )
-from erpnext.stock.utils import calculate_mapped_packed_items_return
 
 form_grid_templates = {
 	"items": "templates/form_grid/item_grid.html"
@@ -744,11 +743,8 @@
 
 	def update_packing_list(self):
 		if cint(self.update_stock) == 1:
-			if cint(self.is_return) and self.return_against:
-				calculate_mapped_packed_items_return(self)
-			else:
-				from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
-				make_packing_list(self)
+			from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
+			make_packing_list(self)
 		else:
 			self.set('packed_items', [])
 
@@ -1411,12 +1407,19 @@
 		frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
 
 	def get_returned_amount(self):
-		returned_amount = frappe.db.sql("""
-			select sum(grand_total)
-			from `tabSales Invoice`
-			where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s
-		""", self.name)
-		return abs(flt(returned_amount[0][0])) if returned_amount else 0
+		from frappe.query_builder.functions import Coalesce, Sum
+		doc = frappe.qb.DocType(self.doctype)
+		returned_amount = (
+			frappe.qb.from_(doc)
+			.select(Sum(doc.grand_total))
+			.where(
+				(doc.docstatus == 1)
+				& (doc.is_return == 1)
+				& (Coalesce(doc.return_against, '') == self.name)
+			)
+		).run()
+
+		return abs(returned_amount[0][0]) if returned_amount[0][0] else 0
 
 	# redeem the loyalty points.
 	def apply_loyalty_points(self):
@@ -1657,7 +1660,6 @@
 @frappe.whitelist()
 def make_delivery_note(source_name, target_doc=None):
 	def set_missing_values(source, target):
-		target.ignore_pricing_rule = 1
 		target.run_method("set_missing_values")
 		target.run_method("set_po_nos")
 		target.run_method("calculate_taxes_and_totals")
diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py
new file mode 100644
index 0000000..2373c8c
--- /dev/null
+++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py
@@ -0,0 +1,134 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import today
+
+from erpnext.accounts.report.general_ledger.general_ledger import execute
+
+
+class TestGeneralLedger(FrappeTestCase):
+
+	def test_foreign_account_balance_after_exchange_rate_revaluation(self):
+		"""
+		Checks the correctness of balance after exchange rate revaluation
+		"""
+		# create a new account with USD currency
+		account_name = "Test USD Account for Revalutation"
+		company = "_Test Company"
+		account = frappe.get_doc({
+			"account_name": account_name,
+			"is_group": 0,
+			"company": company,
+			"root_type": "Asset",
+			"report_type": "Balance Sheet",
+			"account_currency": "USD",
+			"inter_company_account": 0,
+			"parent_account": "Bank Accounts - _TC",
+			"account_type": "Bank",
+			"doctype": "Account"
+		})
+		account.insert(ignore_if_duplicate=True)
+		# create a JV to debit 1000 USD at 75 exchange rate
+		jv = frappe.new_doc("Journal Entry")
+		jv.posting_date = today()
+		jv.company = company
+		jv.multi_currency = 1
+		jv.cost_center = "_Test Cost Center - _TC"
+		jv.set("accounts", [
+			{
+				"account": account.name,
+				"debit_in_account_currency": 1000,
+				"credit_in_account_currency": 0,
+				"exchange_rate": 75,
+				"cost_center": "_Test Cost Center - _TC",
+			},
+			{
+				"account": "Cash - _TC",
+				"debit_in_account_currency": 0,
+				"credit_in_account_currency": 75000,
+				"cost_center": "_Test Cost Center - _TC",
+			},
+		])
+		jv.save()
+		jv.submit()
+		# create a JV to credit 900 USD at 100 exchange rate
+		jv = frappe.new_doc("Journal Entry")
+		jv.posting_date = today()
+		jv.company = company
+		jv.multi_currency = 1
+		jv.cost_center = "_Test Cost Center - _TC"
+		jv.set("accounts", [
+			{
+				"account": account.name,
+				"debit_in_account_currency": 0,
+				"credit_in_account_currency": 900,
+				"exchange_rate": 100,
+				"cost_center": "_Test Cost Center - _TC",
+			},
+			{
+				"account": "Cash - _TC",
+				"debit_in_account_currency": 90000,
+				"credit_in_account_currency": 0,
+				"cost_center": "_Test Cost Center - _TC",
+			},
+		])
+		jv.save()
+		jv.submit()
+
+		# create an exchange rate revaluation entry at 77 exchange rate
+		revaluation = frappe.new_doc("Exchange Rate Revaluation")
+		revaluation.posting_date = today()
+		revaluation.company = company
+		revaluation.set("accounts", [
+			{
+				"account": account.name,
+				"account_currency": "USD",
+				"new_exchange_rate": 77,
+				"new_balance_in_base_currency": 7700,
+				"balance_in_base_currency": -15000,
+				"balance_in_account_currency": 100,
+				"current_exchange_rate": -150
+			}
+		])
+		revaluation.save()
+		revaluation.submit()
+
+		# post journal entry to revaluate
+		frappe.db.set_value('Company',  company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC")
+		revaluation_jv = revaluation.make_jv_entry()
+		revaluation_jv = frappe.get_doc(revaluation_jv)
+		revaluation_jv.cost_center = "_Test Cost Center - _TC"
+		for acc in revaluation_jv.get("accounts"):
+			acc.cost_center = "_Test Cost Center - _TC"
+		revaluation_jv.save()
+		revaluation_jv.submit()
+
+		# check the balance of the account
+		balance = frappe.db.sql(
+			"""
+				select sum(debit_in_account_currency) - sum(credit_in_account_currency)
+				from `tabGL Entry`
+				where account = %s
+				group by account
+			""", account.name)
+
+		self.assertEqual(balance[0][0], 100)
+
+		# check if general ledger shows correct balance
+		columns, data = execute(frappe._dict({
+			"company": company,
+			"from_date": today(),
+			"to_date": today(),
+			"account": [account.name],
+			"group_by": "Group by Voucher (Consolidated)",
+		}))
+
+		self.assertEqual(data[1]["account"], account.name)
+		self.assertEqual(data[1]["debit"], 1000)
+		self.assertEqual(data[1]["credit"], 0)
+		self.assertEqual(data[2]["debit"], 0)
+		self.assertEqual(data[2]["credit"], 900)
+		self.assertEqual(data[3]["debit"], 100)
+		self.assertEqual(data[3]["credit"], 100)
\ No newline at end of file
diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
index d843dfd..70effce 100644
--- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
+++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
@@ -107,6 +107,7 @@
 		select party, sum(debit) as opening_debit, sum(credit) as opening_credit
 		from `tabGL Entry`
 		where company=%(company)s
+			and is_cancelled=0
 			and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
 			and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
 			{account_filter}
@@ -133,6 +134,7 @@
 		select party, sum(debit) as debit, sum(credit) as credit
 		from `tabGL Entry`
 		where company=%(company)s
+			and is_cancelled = 0
 			and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
 			and posting_date >= %(from_date)s and posting_date <= %(to_date)s
 			and ifnull(is_opening, 'No') = 'No'
diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py
index c38e4b8..9e721fb 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -92,10 +92,10 @@
 		account_currency = entry['account_currency']
 
 		if len(account_currencies) == 1 and account_currency == presentation_currency:
-			if entry.get('debit'):
+			if debit_in_account_currency:
 				entry['debit'] = debit_in_account_currency
 
-			if entry.get('credit'):
+			if credit_in_account_currency:
 				entry['credit'] = credit_in_account_currency
 		else:
 			date = currency_info['report_date']
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 2e7d306..2c66542 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -402,7 +402,6 @@
 	frappe.local.message_log = []
 
 def set_missing_values(source, target):
-	target.ignore_pricing_rule = 1
 	target.run_method("set_missing_values")
 	target.run_method("calculate_taxes_and_totals")
 
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index 023c95d..567e41f 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -72,8 +72,8 @@
   "section_break_46",
   "base_grand_total",
   "base_rounding_adjustment",
-  "base_in_words",
   "base_rounded_total",
+  "base_in_words",
   "column_break4",
   "grand_total",
   "rounding_adjustment",
@@ -635,6 +635,7 @@
    "fieldname": "rounded_total",
    "fieldtype": "Currency",
    "label": "Rounded Total",
+   "options": "currency",
    "read_only": 1
   },
   {
@@ -810,7 +811,7 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-12-11 06:43:20.924080",
+ "modified": "2022-03-14 16:13:20.284572",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Supplier Quotation",
@@ -875,6 +876,7 @@
  "show_name_in_global_search": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "timeline_field": "supplier",
  "title_field": "title"
 }
\ No newline at end of file
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
index 171de78..81fc324 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
@@ -109,7 +109,6 @@
 @frappe.whitelist()
 def make_purchase_order(source_name, target_doc=None):
 	def set_missing_values(source, target):
-		target.ignore_pricing_rule = 1
 		target.run_method("set_missing_values")
 		target.run_method("get_schedule_dates")
 		target.run_method("calculate_taxes_and_totals")
diff --git a/erpnext/e_commerce/product_ui/views.js b/erpnext/e_commerce/product_ui/views.js
index 1b5c440..6dce79d 100644
--- a/erpnext/e_commerce/product_ui/views.js
+++ b/erpnext/e_commerce/product_ui/views.js
@@ -495,7 +495,7 @@
 
 			categories.forEach(category => {
 				sub_group_html += `
-					<a href="${ category.route || '#' }" style="text-decoration: none;">
+					<a href="/${ category.route || '#' }" style="text-decoration: none;">
 						<div class="category-pill">
 							${ category.name }
 						</div>
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index 6095413..585059f 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -3,7 +3,7 @@
 
 import frappe
 from frappe.tests.utils import FrappeTestCase
-from frappe.utils import add_days, get_first_day, getdate, now_datetime, nowdate
+from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate
 
 from erpnext.hr.doctype.attendance.attendance import (
 	get_month_map,
@@ -16,6 +16,13 @@
 test_records = frappe.get_test_records('Attendance')
 
 class TestAttendance(FrappeTestCase):
+	def setUp(self):
+		from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
+
+		from_date = get_year_start(getdate())
+		to_date = get_year_ending(getdate())
+		self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
+
 	def test_mark_absent(self):
 		employee = make_employee("test_mark_absent@example.com")
 		date = nowdate()
@@ -31,12 +38,9 @@
 
 		employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
 		frappe.db.delete('Attendance', {'employee': employee})
+		frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list)
 
-		from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
-		holiday_list = make_holiday_list()
-		frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
-
-		first_sunday = get_first_sunday(holiday_list, for_date=first_day)
+		first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
 		mark_attendance(employee, first_day, 'Present')
 		month_name = get_month_name(first_day)
 
@@ -58,11 +62,9 @@
 		employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
 		frappe.db.delete('Attendance', {'employee': employee})
 
-		from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
-		holiday_list = make_holiday_list()
-		frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
+		frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list)
 
-		first_sunday = get_first_sunday(holiday_list, for_date=first_day)
+		first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
 		mark_attendance(employee, first_day, 'Present')
 		month_name = get_month_name(first_day)
 
@@ -87,9 +89,7 @@
 			relieving_date=relieving_date)
 		frappe.db.delete('Attendance', {'employee': employee})
 
-		from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
-		holiday_list = make_holiday_list()
-		frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
+		frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list)
 
 		attendance_date = add_days(first_day, 2)
 		mark_attendance(employee, attendance_date, 'Present')
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 27f98a2..7d32fd8 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -82,7 +82,11 @@
 		set_leave_approver()
 
 		frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"})
-		self.holiday_list = make_holiday_list()
+		frappe.db.set_value("Employee", "_T-Employee-00001", "holiday_list", "")
+
+		from_date = get_year_start(getdate())
+		to_date = get_year_ending(getdate())
+		self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
 
 		if not frappe.db.exists("Leave Type", "_Test Leave Type"):
 			frappe.get_doc(dict(
@@ -316,6 +320,7 @@
 
 		leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company)
 		leave_application.reload()
+
 		# holiday should be excluded while marking attendance
 		self.assertEqual(leave_application.total_leave_days, 3)
 		self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3)
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index a025ff7..15fa67b 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -170,6 +170,7 @@
 			frappe.throw(_("Please select a Company first."), title=_("Mandatory"))
 
 		self.clear_operations()
+		self.clear_inspection()
 		self.validate_main_item()
 		self.validate_currency()
 		self.set_conversion_rate()
@@ -425,6 +426,10 @@
 		if not self.with_operations:
 			self.set('operations', [])
 
+	def clear_inspection(self):
+		if not self.inspection_required:
+			self.quality_inspection_template = None
+
 	def validate_main_item(self):
 		""" Validate main FG item"""
 		item = self.get_item_det(self.item)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 4417123..2f9b9de 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -6,7 +6,6 @@
 from functools import partial
 
 import frappe
-from frappe.test_runner import make_test_records
 from frappe.tests.utils import FrappeTestCase
 from frappe.utils import cstr, flt
 
@@ -20,12 +19,9 @@
 from erpnext.tests.test_subcontracting import set_backflush_based_on
 
 test_records = frappe.get_test_records('BOM')
+test_dependencies = ["Item", "Quality Inspection Template"]
 
 class TestBOM(FrappeTestCase):
-	def setUp(self):
-		if not frappe.get_value('Item', '_Test Item'):
-			make_test_records('Item')
-
 	def test_get_items(self):
 		from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
 		items_dict = get_bom_items_as_dict(bom=get_default_bom(),
@@ -495,6 +491,24 @@
 		self.assertNotEqual(amendment.name, version.name)
 		self.assertEqual(int(version.name.split("-")[-1]), 2)
 
+	def test_clear_inpection_quality(self):
+
+		bom = frappe.copy_doc(test_records[2], ignore_no_copy=True)
+		bom.docstatus = 0
+		bom.is_default = 0
+		bom.quality_inspection_template = "_Test Quality Inspection Template"
+		bom.inspection_required = 1
+		bom.save()
+		bom.reload()
+
+		self.assertEqual(bom.quality_inspection_template, '_Test Quality Inspection Template')
+
+		bom.inspection_required = 0
+		bom.save()
+		bom.reload()
+
+		self.assertEqual(bom.quality_inspection_template, None)
+
 
 def get_default_bom(item_code="_Test FG Item 2"):
 	return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 9f4ace2..5f492d7 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -48,7 +48,7 @@
 		self.validate_work_order()
 
 	def set_sub_operations(self):
-		if self.operation:
+		if not self.sub_operations and self.operation:
 			self.sub_operations = []
 			for row in frappe.get_all('Sub Operation',
 				filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'):
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js
index 8017209..7f60bdc 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_list.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js
@@ -1,4 +1,5 @@
 frappe.listview_settings['Job Card'] = {
+	has_indicator_for_draft: true,
 	get_indicator: function(doc) {
 		if (doc.status === "Work In Progress") {
 			return [__("Work In Progress"), "orange", "status,=,Work In Progress"];
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 3bfb764..339c6af 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -190,7 +190,7 @@
    "label": "Select Items to Manufacture"
   },
   {
-   "depends_on": "get_items_from",
+   "depends_on": "eval:doc.get_items_from && doc.docstatus == 0",
    "fieldname": "get_items",
    "fieldtype": "Button",
    "label": "Get Finished Goods for Manufacture"
@@ -198,6 +198,7 @@
   {
    "fieldname": "po_items",
    "fieldtype": "Table",
+   "label": "Assembly Items",
    "no_copy": 1,
    "options": "Production Plan Item",
    "reqd": 1
@@ -350,6 +351,7 @@
    "hide_border": 1
   },
   {
+   "depends_on": "get_items_from",
    "fieldname": "sub_assembly_items",
    "fieldtype": "Table",
    "label": "Sub Assembly Items",
@@ -357,6 +359,7 @@
    "options": "Production Plan Sub Assembly Item"
   },
   {
+   "depends_on": "eval:doc.po_items && doc.po_items.length && doc.docstatus == 0",
    "fieldname": "get_sub_assembly_items",
    "fieldtype": "Button",
    "label": "Get Sub Assembly Items"
@@ -382,7 +385,7 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-02-23 17:16:10.629378",
+ "modified": "2022-03-14 03:56:23.209247",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Production Plan",
@@ -404,5 +407,6 @@
   }
  ],
  "sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index eeab788..6425374 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -655,6 +655,17 @@
 		]
 		self.assertFalse(pp.all_items_completed())
 
+	def test_production_plan_planned_qty(self):
+		pln = create_production_plan(item_code="_Test FG Item", planned_qty=0.55)
+		pln.make_work_order()
+		work_order = frappe.db.get_value('Work Order', {'production_plan': pln.name}, 'name')
+		wo_doc = frappe.get_doc('Work Order', work_order)
+		wo_doc.update({
+			'wip_warehouse': 'Work In Progress - _TC',
+			'fg_warehouse': 'Finished Goods - _TC'
+		})
+		wo_doc.submit()
+		self.assertEqual(wo_doc.qty, 0.55)
 
 def create_production_plan(**args):
 	"""
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index eaf4de7..6a8136d 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -355,7 +355,6 @@
 		wo_order = make_wo_order_test_record(planned_start_date=now(),
 			sales_order=so.name, qty=3)
 
-		wo_order.submit()
 		self.assertEqual(wo_order.docstatus, 1)
 
 		allow_overproduction("overproduction_percentage_for_sales_order", 0)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 8ec80ad..7eb40ec 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -647,12 +647,12 @@
 		if self.production_plan and self.production_plan_item:
 			qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
 
-			allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings",
+			allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings",
 			"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
 
 			max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0)
 
-			if max_qty < 1:
+			if not max_qty > 0:
 				frappe.throw(_("Cannot produce more item for {0}")
 				.format(self.production_item), OverProductionError)
 			elif self.qty > max_qty:
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index ebda805..16d8c73 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -358,4 +358,5 @@
 erpnext.patches.v14_0.update_batch_valuation_flag
 erpnext.patches.v14_0.delete_non_profit_doctypes
 erpnext.patches.v14_0.update_employee_advance_status
-erpnext.patches.v13_0.add_cost_center_in_loans
\ No newline at end of file
+erpnext.patches.v13_0.add_cost_center_in_loans
+erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items
diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
index 5512543..6f9031f 100644
--- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
+++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
@@ -6,14 +6,14 @@
 
 
 def execute():
+    frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
+    frappe.reload_doc('hr', 'doctype', 'employee_grade')
+    employee_with_assignment = []
+    leave_policy = []
+
     if "leave_policy" in frappe.db.get_table_columns("Employee"):
         employees_with_leave_policy = frappe.db.sql("SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", as_dict = 1)
 
-        employee_with_assignment = []
-        leave_policy =[]
-
-        #for employee
-
         for employee in employees_with_leave_policy:
             alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1})
             if not alloc:
@@ -22,12 +22,10 @@
             employee_with_assignment.append(employee.name)
             leave_policy.append(employee.leave_policy)
 
-
-    if "default_leave_policy" in frappe.db.get_table_columns("Employee"):
+    if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"):
         employee_grade_with_leave_policy = frappe.db.sql("SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", as_dict = 1)
 
         #for whole employee Grade
-
         for grade in employee_grade_with_leave_policy:
             employees = get_employee_with_grade(grade.name)
             for employee in employees:
@@ -47,13 +45,13 @@
                 allocation_exists=True)
 
 def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False):
+    if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2:
+        return
 
     filters = {"employee":employee, "leave_policy": leave_policy}
     if leave_period:
         filters["leave_period"] = leave_period
 
-    frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
-
     if not frappe.db.exists("Leave Policy Assignment" , filters):
         lpa = frappe.new_doc("Leave Policy Assignment")
         lpa.employee = employee
diff --git a/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py b/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py
new file mode 100644
index 0000000..317e85e
--- /dev/null
+++ b/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py
@@ -0,0 +1,34 @@
+import frappe
+
+
+def execute():
+	"""
+	Remove "production_plan_item" field where linked field doesn't exist in tha table.
+	"""
+
+	work_order = frappe.qb.DocType("Work Order")
+	pp_item = frappe.qb.DocType("Production Plan Item")
+
+	broken_work_orders = (
+		frappe.qb
+			.from_(work_order)
+			.left_join(pp_item).on(work_order.production_plan_item == pp_item.name)
+			.select(work_order.name)
+			.where(
+				(work_order.docstatus == 0)
+				& (work_order.production_plan_item.notnull())
+				& (work_order.production_plan_item.like("new-production-plan%"))
+				& (pp_item.name.isnull())
+			)
+	).run(pluck=True)
+
+	if not broken_work_orders:
+		return
+
+	(frappe.qb
+		.update(work_order)
+		.set(work_order.production_plan_item, None)
+		.where(work_order.name.isin(broken_work_orders))
+	).run()
+
+
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index a634dfe..32b0f0f 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -719,7 +719,7 @@
 				where reference_type="Payroll Entry")
 		order by name limit %(start)s, %(page_len)s"""
 		.format(key=searchfield), {
-			'txt': "%%%s%%" % frappe.db.escape(txt),
+			'txt': "%%%s%%" % txt,
 			'start': start, 'page_len': page_len
 		})
 
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 181a2b5..caaed1f 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -1002,7 +1002,7 @@
 
 		# apply rounding
 		if frappe.get_cached_value("Salary Component", row.salary_component, "round_to_the_nearest_integer"):
-			amount, additional_amount = rounded(amount), rounded(additional_amount)
+			amount, additional_amount = rounded(amount or 0), rounded(additional_amount or 0)
 
 		return amount, additional_amount
 
@@ -1279,9 +1279,9 @@
 	def set_base_totals(self):
 		self.base_gross_pay = flt(self.gross_pay) * flt(self.exchange_rate)
 		self.base_total_deduction = flt(self.total_deduction) * flt(self.exchange_rate)
-		self.rounded_total = rounded(self.net_pay)
+		self.rounded_total = rounded(self.net_pay or 0)
 		self.base_net_pay = flt(self.net_pay) * flt(self.exchange_rate)
-		self.base_rounded_total = rounded(self.base_net_pay)
+		self.base_rounded_total = rounded(self.base_net_pay or 0)
 		self.set_net_total_in_words()
 
 	#calculate total working hours, earnings based on hourly wages and totals
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index ebad3cb..5e41b66 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -411,7 +411,7 @@
 	def test_email_salary_slip(self):
 		frappe.db.sql("delete from `tabEmail Queue`")
 
-		make_employee("test_email_salary_slip@salary.com")
+		make_employee("test_email_salary_slip@salary.com", company="_Test Company")
 		ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
 		ss.company = "_Test Company"
 		ss.save()
@@ -1091,18 +1091,19 @@
 def make_holiday_list(list_name=None, from_date=None, to_date=None):
 	fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
 	name = list_name or "Salary Slip Test Holiday List"
-	holiday_list = frappe.db.exists("Holiday List", name)
-	if not holiday_list:
-		holiday_list = frappe.get_doc({
-			"doctype": "Holiday List",
-			"holiday_list_name": name,
-			"from_date": from_date or fiscal_year[1],
-			"to_date": to_date or fiscal_year[2],
-			"weekly_off": "Sunday"
-		}).insert()
-		holiday_list.get_weekly_off_dates()
-		holiday_list.save()
-		holiday_list = holiday_list.name
+
+	frappe.delete_doc_if_exists("Holiday List", name, force=True)
+
+	holiday_list = frappe.get_doc({
+		"doctype": "Holiday List",
+		"holiday_list_name": name,
+		"from_date": from_date or fiscal_year[1],
+		"to_date": to_date or fiscal_year[2],
+		"weekly_off": "Sunday"
+	}).insert()
+	holiday_list.get_weekly_off_dates()
+	holiday_list.save()
+	holiday_list = holiday_list.name
 
 	return holiday_list
 
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 666043b..019496d 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -569,15 +569,12 @@
 }
 
 .scroll-categories {
-	white-space: nowrap;
-	overflow-x: auto;
-
 	.category-pill {
-		margin: 0px 4px;
 		display: inline-block;
-		padding: 6px 12px;
-		background-color: #ecf5fe;
 		width: fit-content;
+		padding: 6px 12px;
+		margin-bottom: 8px;
+		background-color: #ecf5fe;
 		font-size: 14px;
 		border-radius: 18px;
 		color: var(--blue-500);
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
index a03c3f0..515862d 100644
--- a/erpnext/regional/saudi_arabia/utils.py
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -90,7 +90,7 @@
 		tlv_array.append(''.join([tag, length, value]))
 
 		# VAT Amount
-		vat_amount = str(doc.total_taxes_and_charges)
+		vat_amount = str(get_vat_amount(doc))
 
 		tag = bytes([5]).hex()
 		length = bytes([len(vat_amount)]).hex()
@@ -127,6 +127,22 @@
 		doc.db_set('ksa_einv_qr', _file.file_url)
 		doc.notify_update()
 
+def get_vat_amount(doc):
+	vat_settings = frappe.db.get_value('KSA VAT Setting', {'company': doc.company})
+	vat_accounts = []
+	vat_amount = 0
+
+	if vat_settings:
+		vat_settings_doc = frappe.get_cached_doc('KSA VAT Setting', vat_settings)
+
+		for row in vat_settings_doc.get('ksa_vat_sales_accounts'):
+			vat_accounts.append(row.account)
+
+	for tax in doc.get('taxes'):
+		if tax.account_head in vat_accounts:
+			vat_amount += tax.tax_amount
+
+	return vat_amount
 
 def delete_qr_code_file(doc, method=None):
 	region = get_region(doc.company)
diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py
index f350ec4..bdede84 100644
--- a/erpnext/regional/united_arab_emirates/utils.py
+++ b/erpnext/regional/united_arab_emirates/utils.py
@@ -26,9 +26,12 @@
 		elif row.item_code and itemised_tax.get(row.item_code):
 			tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()])
 
-		row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
-		row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
-		row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
+		meta = frappe.get_meta(row.doctype)
+
+		if meta.has_field('tax_rate'):
+			row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
+			row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
+			row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
 
 def get_account_currency(account):
 	"""Helper function to get account currency."""
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index eebde76..06b4ff1 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -165,7 +165,6 @@
 		if source.referral_sales_partner:
 			target.sales_partner=source.referral_sales_partner
 			target.commission_rate=frappe.get_value('Sales Partner', source.referral_sales_partner, 'commission_rate')
-		target.ignore_pricing_rule = 1
 		target.flags.ignore_permissions = ignore_permissions
 		target.run_method("set_missing_values")
 		target.run_method("calculate_taxes_and_totals")
@@ -240,7 +239,7 @@
 		if customer:
 			target.customer = customer.name
 			target.customer_name = customer.customer_name
-		target.ignore_pricing_rule = 1
+
 		target.flags.ignore_permissions = ignore_permissions
 		target.run_method("set_missing_values")
 		target.run_method("calculate_taxes_and_totals")
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index abbb3c9..73e3d19 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -520,7 +520,6 @@
 @frappe.whitelist()
 def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
 	def set_missing_values(source, target):
-		target.ignore_pricing_rule = 1
 		target.run_method("set_missing_values")
 		target.run_method("set_po_nos")
 		target.run_method("calculate_taxes_and_totals")
@@ -596,7 +595,6 @@
 			target.set_advances()
 
 	def set_missing_values(source, target):
-		target.ignore_pricing_rule = 1
 		target.flags.ignore_permissions = True
 		target.run_method("set_missing_values")
 		target.run_method("set_po_nos")
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index 5c7194b..2c53246 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -132,7 +132,8 @@
 	return frappe.get_all(
 		"Item Group",
 		filters=filters,
-		fields=["name", "route"]
+		fields=["name", "route"],
+		order_by="name"
 	)
 
 def get_child_item_groups(item_group_name):
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 75ccd86..ffa2f93 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -13,11 +13,7 @@
 from erpnext.controllers.accounts_controller import get_taxes_and_charges
 from erpnext.controllers.selling_controller import SellingController
 from erpnext.stock.doctype.batch.batch import set_batch_nos
-from erpnext.stock.doctype.serial_no.serial_no import (
-	get_delivery_note_serial_no,
-	update_serial_nos_after_submit,
-)
-from erpnext.stock.utils import calculate_mapped_packed_items_return
+from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
 
 form_grid_templates = {
 	"items": "templates/form_grid/item_grid.html"
@@ -132,12 +128,8 @@
 		self.validate_uom_is_integer("uom", "qty")
 		self.validate_with_previous_doc()
 
-		# Keeps mapped packed_items in case product bundle is updated.
-		if self.is_return and self.return_against:
-			calculate_mapped_packed_items_return(self)
-		else:
-			from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
-			make_packing_list(self)
+		from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
+		make_packing_list(self)
 
 		if self._action != 'submit' and not self.is_return:
 			set_batch_nos(self, 'warehouse', throw=True)
@@ -223,9 +215,6 @@
 		# Updating stock ledger should always be called after updating prevdoc status,
 		# because updating reserved qty in bin depends upon updated delivered qty in SO
 		self.update_stock_ledger()
-		if self.is_return:
-			update_serial_nos_after_submit(self, "items")
-
 		self.make_gl_entries()
 		self.repost_future_sle_and_gle()
 
@@ -445,7 +434,6 @@
 	invoiced_qty_map = get_invoiced_qty_map(source_name)
 
 	def set_missing_values(source, target):
-		target.ignore_pricing_rule = 1
 		target.run_method("set_missing_values")
 		target.run_method("set_po_nos")
 
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index fc3dce1..82f4e7d 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -386,7 +386,8 @@
 		self.assertEqual(actual_qty, 25)
 
 		#  return bundled item
-		dn1 = create_return_delivery_note(source_name=dn.name, rate=500, qty=-2)
+		dn1 = create_delivery_note(item_code='_Test Product Bundle Item', is_return=1,
+			return_against=dn.name, qty=-2, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1")
 
 		# qty after return
 		actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1")
@@ -822,20 +823,6 @@
 
 		automatically_fetch_payment_terms(enable=0)
 
-	def test_standalone_serial_no_return(self):
-		dn = create_delivery_note(item_code="_Test Serialized Item With Series", is_return=True, qty=-1)
-		dn.reload()
-		self.assertTrue(dn.items[0].serial_no)
-
-def create_return_delivery_note(**args):
-	args = frappe._dict(args)
-	from erpnext.controllers.sales_and_purchase_return import make_return_doc
-	doc = make_return_doc("Delivery Note", args.source_name, None)
-	doc.items[0].rate = args.rate
-	doc.items[0].qty = args.qty
-	doc.submit()
-	return doc
-
 def create_delivery_note(**args):
 	dn = frappe.new_doc("Delivery Note")
 	args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py
index 94268a8..5f1b954 100644
--- a/erpnext/stock/doctype/packed_item/test_packed_item.py
+++ b/erpnext/stock/doctype/packed_item/test_packed_item.py
@@ -17,15 +17,25 @@
 	@classmethod
 	def setUpClass(cls) -> None:
 		super().setUpClass()
+		cls.warehouse = "_Test Warehouse - _TC"
 		cls.bundle = "_Test Product Bundle X"
 		cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"]
+
+		cls.bundle2 = "_Test Product Bundle Y"
+		cls.bundle2_items = ["_Test Bundle Item 3", "_Test Bundle Item 4"]
+
 		make_item(cls.bundle, {"is_stock_item": 0})
-		for item in cls.bundle_items:
+		make_item(cls.bundle2, {"is_stock_item": 0})
+		for item in cls.bundle_items + cls.bundle2_items:
 			make_item(item, {"is_stock_item": 1})
 
 		make_item("_Test Normal Stock Item", {"is_stock_item": 1})
 
 		make_product_bundle(cls.bundle, cls.bundle_items, qty=2)
+		make_product_bundle(cls.bundle2, cls.bundle2_items, qty=2)
+
+		for item in cls.bundle_items + cls.bundle2_items:
+			make_stock_entry(item_code=item, to_warehouse=cls.warehouse, qty=100, rate=100)
 
 	def test_adding_bundle_item(self):
 		"Test impact on packed items if bundle item row is added."
@@ -156,3 +166,104 @@
 		credit_after_reposting = sum(gle.credit for gle in gles)
 		self.assertNotEqual(credit_before_repost, credit_after_reposting)
 		self.assertAlmostEqual(credit_after_reposting, 2 * credit_before_repost)
+
+	def assertReturns(self, original, returned):
+		self.assertEqual(len(original), len(returned))
+
+		sort_function = lambda p: (p.parent_item, p.item_code, p.qty)
+
+		for sent, returned in zip(
+			sorted(original, key=sort_function),
+			sorted(returned, key=sort_function)
+		):
+			self.assertEqual(sent.item_code, returned.item_code)
+			self.assertEqual(sent.parent_item, returned.parent_item)
+			self.assertEqual(sent.qty, -1 * returned.qty)
+
+	def test_returning_full_bundles(self):
+		from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+
+		item_list = [
+			{
+				"item_code": self.bundle,
+				"warehouse": self.warehouse,
+				"qty": 1,
+				"rate": 100,
+			},
+			{
+				"item_code": self.bundle2,
+				"warehouse": self.warehouse,
+				"qty": 1,
+				"rate": 100,
+			}
+		]
+		so = make_sales_order(item_list=item_list, warehouse=self.warehouse)
+
+		dn = make_delivery_note(so.name)
+		dn.save()
+		dn.submit()
+
+		# create return
+		dn_ret = make_sales_return(dn.name)
+		dn_ret.save()
+		dn_ret.submit()
+		self.assertReturns(dn.packed_items, dn_ret.packed_items)
+
+	def test_returning_partial_bundles(self):
+		from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+
+		item_list = [
+			{
+				"item_code": self.bundle,
+				"warehouse": self.warehouse,
+				"qty": 1,
+				"rate": 100,
+			},
+			{
+				"item_code": self.bundle2,
+				"warehouse": self.warehouse,
+				"qty": 1,
+				"rate": 100,
+			}
+		]
+		so = make_sales_order(item_list=item_list, warehouse=self.warehouse)
+
+		dn = make_delivery_note(so.name)
+		dn.save()
+		dn.submit()
+
+		# create return
+		dn_ret = make_sales_return(dn.name)
+		# remove bundle 2
+		dn_ret.items.pop()
+
+		dn_ret.save()
+		dn_ret.submit()
+		dn_ret.reload()
+
+		self.assertTrue(all(d.parent_item == self.bundle for d in dn_ret.packed_items))
+
+		expected_returns = [d for d in dn.packed_items if d.parent_item == self.bundle]
+		self.assertReturns(expected_returns, dn_ret.packed_items)
+
+
+	def test_returning_partial_bundle_qty(self):
+		from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+
+		so = make_sales_order(item_code=self.bundle, warehouse=self.warehouse, qty = 2)
+
+		dn = make_delivery_note(so.name)
+		dn.save()
+		dn.submit()
+
+		# create return
+		dn_ret = make_sales_return(dn.name)
+		# halve the qty
+		dn_ret.items[0].qty = -1
+		dn_ret.save()
+		dn_ret.submit()
+
+		expected_returns = dn.packed_items
+		for d in expected_returns:
+			d.qty /= 2
+		self.assertReturns(expected_returns, dn_ret.packed_items)
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 52f10ea..5bb337e 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -762,7 +762,6 @@
 			frappe.throw(_("All items have already been Invoiced/Returned"))
 
 		doc = frappe.get_doc(target)
-		doc.ignore_pricing_rule = 1
 		doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company)
 		doc.run_method("onload")
 		doc.run_method("set_missing_values")
diff --git a/erpnext/stock/doctype/quality_inspection_template/test_records.json b/erpnext/stock/doctype/quality_inspection_template/test_records.json
new file mode 100644
index 0000000..980f49a
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_template/test_records.json
@@ -0,0 +1,13 @@
+[
+ {
+     "quality_inspection_template_name" : "_Test Quality Inspection Template",
+     "doctype": "Quality Inspection Template",
+     "item_quality_inspection_parameter" : [
+           {
+            "specification": "_Test Param",
+            "doctype": "Item Quality Inspection Parameter",
+            "parentfield": "item_quality_inspection_parameter"
+           }
+     ]
+ }
+]
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index f4d52ad..f8ec784 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -45,11 +45,21 @@
 			self.db_set('status', self.status)
 
 	def on_submit(self):
-		if not frappe.flags.in_test or self.flags.dont_run_in_test or frappe.flags.dont_execute_stock_reposts:
+		"""During tests reposts are executed immediately.
+
+		Exceptions:
+			1. "Repost Item Valuation" document has self.flags.dont_run_in_test
+			2. global flag frappe.flags.dont_execute_stock_reposts is set
+
+			These flags are useful for asserting real time behaviour like quantity updates.
+		"""
+
+		if not frappe.flags.in_test:
+			return
+		if self.flags.dont_run_in_test or frappe.flags.dont_execute_stock_reposts:
 			return
 
-		frappe.enqueue(repost, timeout=1800, queue='long',
-			job_name='repost_sle', now=frappe.flags.in_test, doc=self)
+		repost(self)
 
 	@frappe.whitelist()
 	def restart_reposting(self):
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 6a2e7fb..2352235 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
@@ -10,10 +10,7 @@
 from frappe.tests.utils import FrappeTestCase
 from frappe.utils import add_days, today
 
-from erpnext.stock.doctype.delivery_note.test_delivery_note import (
-	create_delivery_note,
-	create_return_delivery_note,
-)
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
 	create_landed_cost_voucher,
@@ -239,7 +236,8 @@
 		self.assertEqual(outgoing_rate, 100)
 
 		# Return Entry: Qty = -2, Rate = 150
-		return_dn = create_return_delivery_note(source_name=dn.name, rate=150, qty=-2)
+		return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150,
+			company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
 
 		# check incoming rate for Return entry
 		incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js
index cc0e2cf..89ac4b5 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.js
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.js
@@ -13,5 +13,24 @@
 
 		frm.set_query("default_warehouse", filters);
 		frm.set_query("sample_retention_warehouse", filters);
+	},
+	allow_negative_stock: function(frm) {
+		if (!frm.doc.allow_negative_stock) {
+			return;
+		}
+
+		let msg = __("Using negative stock disables FIFO/Moving average valuation when inventory is negative.");
+		msg += " ";
+		msg += __("This is considered dangerous from accounting point of view.")
+		msg += "<br>";
+		msg += ("Do you still want to enable negative inventory?");
+
+		frappe.confirm(
+			msg,
+			() => {},
+			() => {
+				frm.set_value("allow_negative_stock", 0);
+			}
+		);
 	}
 });
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 81fa045..9fde47e 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -7,6 +7,7 @@
 from frappe.utils import cint, flt
 
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for
 from erpnext.stock.utils import (
 	is_reposting_item_valuation_in_progress,
 	update_included_uom_in_report,
@@ -70,7 +71,10 @@
 	serial_nos = get_serial_nos(sle.serial_no)
 	key = (sle.item_code, sle.warehouse)
 	if key not in available_serial_nos:
-		available_serial_nos.setdefault(key, [])
+		stock_balance = get_stock_balance_for(sle.item_code, sle.warehouse, sle.date.split(' ')[0],
+			sle.date.split(' ')[1])
+		serials = get_serial_nos(stock_balance['serial_nos']) if stock_balance['serial_nos'] else []
+		available_serial_nos.setdefault(key, serials)
 
 	existing_serial_no = available_serial_nos[key]
 	for sn in serial_nos:
diff --git a/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py
new file mode 100644
index 0000000..163b205
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, today
+
+from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
+	make_serial_item_with_serial,
+)
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.report.stock_ledger.stock_ledger import execute
+
+
+class TestStockLedgerReeport(FrappeTestCase):
+	def setUp(self) -> None:
+		make_serial_item_with_serial("_Test Stock Report Serial Item")
+		self.filters = frappe._dict(
+			company="_Test Company", from_date=today(), to_date=add_days(today(), 30),
+			item_code="_Test Stock Report Serial Item"
+		)
+
+	def tearDown(self) -> None:
+		frappe.db.rollback()
+
+	def test_serial_balance(self):
+		item_code = "_Test Stock Report Serial Item"
+		# Checks serials which were added through stock in entry.
+		columns, data = execute(self.filters)
+		self.assertEqual(data[0].in_qty, 2)
+		serials_added = get_serial_nos(data[0].serial_no)
+		self.assertEqual(len(serials_added), 2)
+		# Stock out entry for one of the serials.
+		dn = create_delivery_note(item=item_code, serial_no=serials_added[1])
+		self.filters.voucher_no = dn.name
+		columns, data = execute(self.filters)
+		self.assertEqual(data[0].out_qty, -1)
+		self.assertEqual(data[0].serial_no, serials_added[1])
+		self.assertEqual(data[0].balance_serial_no, serials_added[0])
+
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 353bfa4..1aaaad2 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -1040,7 +1040,6 @@
 
 	if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \
 			and cint(erpnext.is_perpetual_inventory_enabled(company)):
-		frappe.local.message_log = []
 		form_link = get_link_to_form("Item", item_code)
 
 		message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no)
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index f85a04f..e205389 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -422,19 +422,6 @@
 	if reposting_in_progress:
 		frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
 
-
-def calculate_mapped_packed_items_return(return_doc):
-	parent_items = set([item.parent_item for item in return_doc.packed_items])
-	against_doc = frappe.get_doc(return_doc.doctype, return_doc.return_against)
-
-	for original_bundle, returned_bundle in zip(against_doc.items, return_doc.items):
-		if original_bundle.item_code in parent_items:
-			for returned_packed_item, original_packed_item in zip(return_doc.packed_items, against_doc.packed_items):
-				if returned_packed_item.parent_item == original_bundle.item_code:
-					returned_packed_item.parent_detail_docname = returned_bundle.name
-					returned_packed_item.qty = (original_packed_item.qty / original_bundle.qty) * returned_bundle.qty
-
-
 def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool:
 	"""Check if there are pending reposting job till the specified posting date."""
 
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index b882b9d..1a24f2b 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -987,15 +987,15 @@
 Expected End Date,Voraussichtliches Enddatum,
 Expected Hrs,Erwartete Stunden,
 Expected Start Date,Voraussichtliches Startdatum,
-Expense,Auslage,
+Expense,Aufwand,
 Expense / Difference account ({0}) must be a 'Profit or Loss' account,"Aufwands-/Differenz-Konto ({0}) muss ein ""Gewinn oder Verlust""-Konto sein",
 Expense Account,Aufwandskonto,
-Expense Claim,Aufwandsabrechnung,
+Expense Claim,Auslagenabrechnung,
 Expense Claim for Vehicle Log {0},Auslagenabrechnung für Fahrtenbuch {0},
 Expense Claim {0} already exists for the Vehicle Log,Auslagenabrechnung {0} existiert bereits für das Fahrzeug Log,
 Expense Claims,Aufwandsabrechnungen,
 Expense account is mandatory for item {0},Aufwandskonto ist zwingend für Artikel {0},
-Expenses,Ausgaben,
+Expenses,Aufwendungen,
 Expenses Included In Asset Valuation,"Aufwendungen, die in der Vermögensbewertung enthalten sind",
 Expenses Included In Valuation,In der Bewertung enthaltene Aufwendungen,
 Expired Batches,Abgelaufene Chargen,
@@ -1541,7 +1541,7 @@
 Marketing Expenses,Marketingkosten,
 Marketplace,Marktplatz,
 Marketplace Error,Marktplatzfehler,
-Masters,Stämme,
+Masters,Stammdaten,
 Match Payments with Invoices,Zahlungen und Rechnungen abgleichen,
 Match non-linked Invoices and Payments.,Nicht verknüpfte Rechnungen und Zahlungen verknüpfen,
 Material,Material,
@@ -1621,7 +1621,7 @@
 More than one selection for {0} not allowed,Mehr als eine Auswahl für {0} ist nicht zulässig,
 More...,Mehr...,
 Motion Picture & Video,Film & Fernsehen,
-Move,Bewegen,
+Move,Verschieben,
 Move Item,Element verschieben,
 Multi Currency,Unterschiedliche Währungen,
 Multiple Item prices.,Mehrere verschiedene Artikelpreise,
@@ -1943,7 +1943,7 @@
 Pharmaceuticals,Pharmaprodukte,
 Physician,Arzt,
 Piecework,Akkordarbeit,
-Pincode,Postleitzahl (PLZ),
+Pincode,Postleitzahl,
 Place Of Supply (State/UT),Ort der Lieferung (Staat / UT),
 Place Order,Bestellung aufgeben,
 Plan Name,Planname,
@@ -2539,7 +2539,7 @@
 Sales Partner,Vertriebspartner,
 Sales Pipeline,Vertriebspipeline,
 Sales Price List,Verkaufspreisliste,
-Sales Return,Rücklieferung,
+Sales Return,Retoure,
 Sales Summary,Verkaufszusammenfassung,
 Sales Tax Template,Umsatzsteuer-Vorlage,
 Sales Team,Verkaufsteam,
@@ -9845,3 +9845,8 @@
 Creating Purchase Order ...,Bestellung anlegen ...,
 "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.","Wählen Sie einen Lieferanten aus den Standardlieferanten der folgenden Artikel aus. Bei der Auswahl erfolgt eine Bestellung nur für Artikel, die dem ausgewählten Lieferanten gehören.",
 Row #{}: You must select {} serial numbers for item {}.,Zeile # {}: Sie müssen {} Seriennummern für Artikel {} auswählen.,
+{}  To Deliver,{} Zu liefern,
+{}  To Receive,{} Zu erhalten,
+{}  Available,{} Verfügbar,
+Report an Issue,Ein Problem melden,
+User Forum,Anwenderforum,
diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.js b/erpnext/utilities/doctype/rename_tool/rename_tool.js
index 5553e44..b8203f4 100644
--- a/erpnext/utilities/doctype/rename_tool/rename_tool.js
+++ b/erpnext/utilities/doctype/rename_tool/rename_tool.js
@@ -30,7 +30,16 @@
 					select_doctype: frm.doc.select_doctype
 				},
 				callback: function(r) {
-					frm.get_field("rename_log").$wrapper.html(r.message.join("<br>"));
+					let html = r.message.join("<br>");
+
+					if (r.exc) {
+						r.exc = frappe.utils.parse_json(r.exc);
+						if (Array.isArray(r.exc)) {
+							html += "<br>" + r.exc.join("<br>");
+						}
+					}
+
+					frm.get_field("rename_log").$wrapper.html(html);
 				}
 			});
 		});