Merge pull request #29879 from deepeshgarg007/misc_fixes

fix: Validate party account with company
diff --git a/.mergify.yml b/.mergify.yml
index f3d0409..b7d1df4 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -14,9 +14,39 @@
       close:
       comment:
           message: |
-            @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch. 
+            @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
             https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
 
+  - name: backport to develop
+    conditions:
+      - label="backport develop"
+    actions:
+      backport:
+        branches:
+          - develop
+        assignees:
+          - "{{ author }}"
+
+  - name: backport to version-14-hotfix
+    conditions:
+      - label="backport version-14-hotfix"
+    actions:
+      backport:
+        branches:
+          - version-14-hotfix
+        assignees:
+          - "{{ author }}"
+
+  - name: backport to version-14-pre-release
+    conditions:
+      - label="backport version-14-pre-release"
+    actions:
+      backport:
+        branches:
+          - version-14-pre-release
+        assignees:
+          - "{{ author }}"
+
   - name: backport to version-13-hotfix
     conditions:
       - label="backport version-13-hotfix"
@@ -55,4 +85,4 @@
         branches:
           - version-12-pre-release
         assignees:
-          - "{{ author }}"
\ No newline at end of file
+          - "{{ author }}"
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 4211bd0..f3351dd 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -7,6 +7,7 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
 from frappe.utils import flt
 
 from erpnext import get_company_currency
@@ -275,6 +276,10 @@
 		}
 
 	matching_vouchers = []
+
+	matching_vouchers.extend(get_loan_vouchers(bank_account, transaction,
+		document_types, filters))
+
 	for query in subquery:
 		matching_vouchers.extend(
 			frappe.db.sql(query, filters,)
@@ -311,6 +316,114 @@
 
 	return queries
 
+def get_loan_vouchers(bank_account, transaction, document_types, filters):
+	vouchers = []
+	amount_condition = True if "exact_match" in document_types else False
+
+	if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
+		vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
+
+	if transaction.deposit > 0 and "loan_repayment" in document_types:
+		vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
+
+	return vouchers
+
+def get_ld_matching_query(bank_account, amount_condition, filters):
+	loan_disbursement = frappe.qb.DocType("Loan Disbursement")
+	matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
+	matching_party = loan_disbursement.applicant_type == filters.get("party_type") and \
+			loan_disbursement.applicant == filters.get("party")
+
+	rank = (
+			frappe.qb.terms.Case()
+			.when(matching_reference, 1)
+			.else_(0)
+		)
+
+	rank1 = (
+			frappe.qb.terms.Case()
+			.when(matching_party, 1)
+			.else_(0)
+		)
+
+	query = frappe.qb.from_(loan_disbursement).select(
+		rank + rank1 + 1,
+		ConstantColumn("Loan Disbursement").as_("doctype"),
+		loan_disbursement.name,
+		loan_disbursement.disbursed_amount,
+		loan_disbursement.reference_number,
+		loan_disbursement.reference_date,
+		loan_disbursement.applicant_type,
+		loan_disbursement.disbursement_date
+	).where(
+		loan_disbursement.docstatus == 1
+	).where(
+		loan_disbursement.clearance_date.isnull()
+	).where(
+		loan_disbursement.disbursement_account == bank_account
+	)
+
+	if amount_condition:
+		query.where(
+			loan_disbursement.disbursed_amount == filters.get('amount')
+		)
+	else:
+		query.where(
+			loan_disbursement.disbursed_amount <= filters.get('amount')
+		)
+
+	vouchers = query.run(as_list=True)
+
+	return vouchers
+
+def get_lr_matching_query(bank_account, amount_condition, filters):
+	loan_repayment = frappe.qb.DocType("Loan Repayment")
+	matching_reference = loan_repayment.reference_number == filters.get("reference_number")
+	matching_party = loan_repayment.applicant_type == filters.get("party_type") and \
+			loan_repayment.applicant == filters.get("party")
+
+	rank = (
+			frappe.qb.terms.Case()
+			.when(matching_reference, 1)
+			.else_(0)
+		)
+
+	rank1 = (
+			frappe.qb.terms.Case()
+			.when(matching_party, 1)
+			.else_(0)
+		)
+
+	query = frappe.qb.from_(loan_repayment).select(
+		rank + rank1 + 1,
+		ConstantColumn("Loan Repayment").as_("doctype"),
+		loan_repayment.name,
+		loan_repayment.amount_paid,
+		loan_repayment.reference_number,
+		loan_repayment.reference_date,
+		loan_repayment.applicant_type,
+		loan_repayment.posting_date
+	).where(
+		loan_repayment.docstatus == 1
+	).where(
+		loan_repayment.clearance_date.isnull()
+	).where(
+		loan_repayment.payment_account == bank_account
+	)
+
+	if amount_condition:
+		query.where(
+			loan_repayment.amount_paid == filters.get('amount')
+		)
+	else:
+		query.where(
+			loan_repayment.amount_paid <= filters.get('amount')
+		)
+
+	vouchers = query.run()
+
+	return vouchers
+
 def get_pe_matching_query(amount_condition, account_from_to, transaction):
 	# get matching payment entries query
 	if transaction.deposit > 0:
@@ -348,7 +461,6 @@
 	# We have mapping at the bank level
 	# So one bank could have both types of bank accounts like asset and liability
 	# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
-	company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
 	cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
 
 	return f"""
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 51e1d6e..a476cab 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -49,7 +49,8 @@
 
 	def clear_linked_payment_entries(self, for_cancel=False):
 		for payment_entry in self.payment_entries:
-			if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
+			if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim", "Loan Repayment",
+				"Loan Disbursement"]:
 				self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
 
 			elif payment_entry.payment_document == "Sales Invoice":
@@ -116,11 +117,18 @@
 			payment_entry.payment_entry, paid_amount_field)
 
 	elif payment_entry.payment_document == "Journal Entry":
-		return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
+		return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account},
+			"sum(credit_in_account_currency)")
 
 	elif payment_entry.payment_document == "Expense Claim":
 		return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
 
+	elif payment_entry.payment_document == "Loan Disbursement":
+		return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount")
+
+	elif payment_entry.payment_document == "Loan Repayment":
+		return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
+
 	else:
 		frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))
 
diff --git a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
index 22cf797..02c6875 100644
--- a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
+++ b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
@@ -1,94 +1,34 @@
 {
- "allow_copy": 0, 
- "allow_guest_to_view": 0, 
- "allow_import": 0, 
- "allow_rename": 0, 
- "autoname": "field:mapping", 
- "beta": 0, 
- "creation": "2018-02-08 10:18:48.513608", 
- "custom": 0, 
- "docstatus": 0, 
- "doctype": "DocType", 
- "document_type": "", 
- "editable_grid": 1, 
- "engine": "InnoDB", 
+ "actions": [],
+ "creation": "2018-02-08 10:18:48.513608",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "mapping"
+ ],
  "fields": [
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "mapping", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Mapping", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "Cash Flow Mapping", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
+   "fieldname": "mapping",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Mapping",
+   "options": "Cash Flow Mapping",
+   "reqd": 1,
+   "unique": 1
   }
- ], 
- "has_web_view": 0, 
- "hide_heading": 0, 
- "hide_toolbar": 0, 
- "idx": 0, 
- "image_view": 0, 
- "in_create": 0, 
- "is_submittable": 0, 
- "issingle": 0, 
- "istable": 0, 
- "max_attachments": 0, 
- "modified": "2018-02-08 10:33:39.413930", 
- "modified_by": "Administrator", 
- "module": "Accounts", 
- "name": "Cash Flow Mapping Template Details", 
- "name_case": "", 
- "owner": "Administrator", 
- "permissions": [
-  {
-   "amend": 0, 
-   "apply_user_permissions": 0, 
-   "cancel": 0, 
-   "create": 1, 
-   "delete": 1, 
-   "email": 1, 
-   "export": 1, 
-   "if_owner": 0, 
-   "import": 0, 
-   "permlevel": 0, 
-   "print": 1, 
-   "read": 1, 
-   "report": 1, 
-   "role": "System Manager", 
-   "set_user_permissions": 0, 
-   "share": 1, 
-   "submit": 0, 
-   "write": 1
-  }
- ], 
- "quick_entry": 1, 
- "read_only": 0, 
- "read_only_onload": 0, 
- "show_name_in_global_search": 0, 
- "sort_field": "modified", 
- "sort_order": "DESC", 
- "track_changes": 1, 
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2022-02-21 03:34:57.902332",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Cash Flow Mapping Template Details",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
index 6c401fb..b72d266 100644
--- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
+++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
@@ -4,7 +4,12 @@
 
 import frappe
 from frappe import _
-from frappe.utils import flt, getdate, nowdate
+from frappe.query_builder.custom import ConstantColumn
+from frappe.query_builder.functions import Sum
+from frappe.utils import flt, getdate
+from pypika import CustomFunction
+
+from erpnext.accounts.utils import get_balance_on
 
 
 def execute(filters=None):
@@ -18,7 +23,6 @@
 
 	data = get_entries(filters)
 
-	from erpnext.accounts.utils import get_balance_on
 	balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
 
 	total_debit, total_credit = 0,0
@@ -118,7 +122,21 @@
 	]
 
 def get_entries(filters):
-	journal_entries = frappe.db.sql("""
+	journal_entries = get_journal_entries(filters)
+
+	payment_entries = get_payment_entries(filters)
+
+	loan_entries = get_loan_entries(filters)
+
+	pos_entries = []
+	if filters.include_pos_transactions:
+		pos_entries = get_pos_entries(filters)
+
+	return sorted(list(payment_entries)+list(journal_entries+list(pos_entries) + list(loan_entries)),
+			key=lambda k: getdate(k['posting_date']))
+
+def get_journal_entries(filters):
+	return frappe.db.sql("""
 		select "Journal Entry" as payment_document, jv.posting_date,
 			jv.name as payment_entry, jvd.debit_in_account_currency as debit,
 			jvd.credit_in_account_currency as credit, jvd.against_account,
@@ -130,7 +148,8 @@
 			and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
 			and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1)
 
-	payment_entries = frappe.db.sql("""
+def get_payment_entries(filters):
+	return frappe.db.sql("""
 		select
 			"Payment Entry" as payment_document, name as payment_entry,
 			reference_no, reference_date as ref_date,
@@ -145,9 +164,8 @@
 			and ifnull(clearance_date, '4000-01-01') > %(report_date)s
 	""", filters, as_dict=1)
 
-	pos_entries = []
-	if filters.include_pos_transactions:
-		pos_entries = frappe.db.sql("""
+def get_pos_entries(filters):
+	return frappe.db.sql("""
 			select
 				"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
 				si.posting_date, si.debit_to as against_account, sip.clearance_date,
@@ -161,8 +179,42 @@
 				si.posting_date ASC, si.name DESC
 		""", filters, as_dict=1)
 
-	return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)),
-			key=lambda k: k['posting_date'] or getdate(nowdate()))
+def get_loan_entries(filters):
+	loan_docs = []
+	for doctype in ["Loan Disbursement", "Loan Repayment"]:
+		loan_doc = frappe.qb.DocType(doctype)
+		ifnull = CustomFunction('IFNULL', ['value', 'default'])
+
+		if doctype == "Loan Disbursement":
+			amount_field = (loan_doc.disbursed_amount).as_("credit")
+			posting_date = (loan_doc.disbursement_date).as_("posting_date")
+			account = loan_doc.disbursement_account
+		else:
+			amount_field = (loan_doc.amount_paid).as_("debit")
+			posting_date = (loan_doc.posting_date).as_("posting_date")
+			account = loan_doc.payment_account
+
+		entries = frappe.qb.from_(loan_doc).select(
+			ConstantColumn(doctype).as_("payment_document"),
+			(loan_doc.name).as_("payment_entry"),
+			(loan_doc.reference_number).as_("reference_no"),
+			(loan_doc.reference_date).as_("ref_date"),
+			amount_field,
+			posting_date,
+		).where(
+			loan_doc.docstatus == 1
+		).where(
+			account == filters.get('account')
+		).where(
+			posting_date <= getdate(filters.get('report_date'))
+		).where(
+			ifnull(loan_doc.clearance_date, '4000-01-01') > getdate(filters.get('report_date'))
+		).run(as_dict=1)
+
+		loan_docs.extend(entries)
+
+	return loan_docs
+
 
 def get_amounts_not_reflected_in_system(filters):
 	je_amount = frappe.db.sql("""
@@ -182,7 +234,40 @@
 
 	pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
 
-	return je_amount + pe_amount
+	loan_amount = get_loan_amount(filters)
+
+	return je_amount + pe_amount + loan_amount
+
+def get_loan_amount(filters):
+	total_amount = 0
+	for doctype in ["Loan Disbursement", "Loan Repayment"]:
+		loan_doc = frappe.qb.DocType(doctype)
+		ifnull = CustomFunction('IFNULL', ['value', 'default'])
+
+		if doctype == "Loan Disbursement":
+			amount_field = Sum(loan_doc.disbursed_amount)
+			posting_date = (loan_doc.disbursement_date).as_("posting_date")
+			account = loan_doc.disbursement_account
+		else:
+			amount_field = Sum(loan_doc.amount_paid)
+			posting_date = (loan_doc.posting_date).as_("posting_date")
+			account = loan_doc.payment_account
+
+		amount = frappe.qb.from_(loan_doc).select(
+			amount_field
+		).where(
+			loan_doc.docstatus == 1
+		).where(
+			account == filters.get('account')
+		).where(
+			posting_date > getdate(filters.get('report_date'))
+		).where(
+			ifnull(loan_doc.clearance_date, '4000-01-01') <= getdate(filters.get('report_date'))
+		).run()[0][0]
+
+		total_amount += flt(amount)
+
+	return amount
 
 def get_balance_row(label, amount, account_currency):
 	if amount > 0:
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 758e3e9..1e20f7b 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -354,9 +354,6 @@
 		if d.parent_account:
 			account = d.parent_account_name
 
-			# if not accounts_by_name.get(account):
-			# 	continue
-
 			for company in companies:
 				accounts_by_name[account][company] = \
 					accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0)
@@ -367,7 +364,7 @@
 				accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
 
 def get_account_heads(root_type, companies, filters):
-	accounts = get_accounts(root_type, filters)
+	accounts = get_accounts(root_type, companies)
 
 	if not accounts:
 		return None, None, None
@@ -396,7 +393,7 @@
 
 	for account in accounts:
 		if account.parent_account:
-			account["parent_account_name"] = name_to_account_map[account.parent_account]
+			account["parent_account_name"] = name_to_account_map.get(account.parent_account)
 
 	return accounts
 
@@ -419,12 +416,19 @@
 	return frappe.db.sql_list("""select name from `tabCompany`
 		where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt))
 
-def get_accounts(root_type, filters):
-	return frappe.db.sql(""" select name, is_group, company,
-			parent_account, lft, rgt, root_type, report_type, account_name, account_number
-		from
-			`tabAccount` where company = %s and root_type = %s
-		""" , (filters.get('company'), root_type), as_dict=1)
+def get_accounts(root_type, companies):
+	accounts = []
+	added_accounts = []
+
+	for company in companies:
+		for account in frappe.get_all("Account", fields=["name", "is_group", "company",
+			"parent_account", "lft", "rgt", "root_type", "report_type", "account_name", "account_number"],
+			filters={"company": company, "root_type": root_type}):
+			if account.account_name not in added_accounts:
+				accounts.append(account)
+				added_accounts.append(account.account_name)
+
+	return accounts
 
 def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
 	data = []
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js
index 2ba649d..158ff4d 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.js
+++ b/erpnext/accounts/report/gross_profit/gross_profit.js
@@ -8,20 +8,22 @@
 			"label": __("Company"),
 			"fieldtype": "Link",
 			"options": "Company",
-			"reqd": 1,
-			"default": frappe.defaults.get_user_default("Company")
+			"default": frappe.defaults.get_user_default("Company"),
+			"reqd": 1
 		},
 		{
 			"fieldname":"from_date",
 			"label": __("From Date"),
 			"fieldtype": "Date",
-			"default": frappe.defaults.get_user_default("year_start_date")
+			"default": frappe.defaults.get_user_default("year_start_date"),
+			"reqd": 1
 		},
 		{
 			"fieldname":"to_date",
 			"label": __("To Date"),
 			"fieldtype": "Date",
-			"default": frappe.defaults.get_user_default("year_end_date")
+			"default": frappe.defaults.get_user_default("year_end_date"),
+			"reqd": 1
 		},
 		{
 			"fieldname":"sales_invoice",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json
index 76c560a..0730ffd 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.json
+++ b/erpnext/accounts/report/gross_profit/gross_profit.json
@@ -1,5 +1,5 @@
 {
- "add_total_row": 0,
+ "add_total_row": 1,
  "columns": [],
  "creation": "2013-02-25 17:03:34",
  "disable_prepared_report": 0,
@@ -9,7 +9,7 @@
  "filters": [],
  "idx": 3,
  "is_standard": "Yes",
- "modified": "2021-11-13 19:14:23.730198",
+ "modified": "2022-02-11 10:18:36.956558",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Gross Profit",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 84effc0..b03bb9b 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -70,43 +70,42 @@
 		data.append(row)
 
 def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
-	for idx, src in enumerate(gross_profit_data.grouped_data):
+	for src in gross_profit_data.grouped_data:
 		row = []
 		for col in group_wise_columns.get(scrub(filters.group_by)):
 			row.append(src.get(col))
 
 		row.append(filters.currency)
-		if idx == len(gross_profit_data.grouped_data)-1:
-			row[0] = "Total"
 
 		data.append(row)
 
 def get_columns(group_wise_columns, filters):
 	columns = []
 	column_map = frappe._dict({
-		"parent": _("Sales Invoice") + ":Link/Sales Invoice:120",
-		"invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120",
-		"posting_date": _("Posting Date") + ":Date:100",
-		"posting_time": _("Posting Time") + ":Data:100",
-		"item_code": _("Item Code") + ":Link/Item:100",
-		"item_name": _("Item Name") + ":Data:100",
-		"item_group": _("Item Group") + ":Link/Item Group:100",
-		"brand": _("Brand") + ":Link/Brand:100",
-		"description": _("Description") +":Data:100",
-		"warehouse": _("Warehouse") + ":Link/Warehouse:100",
-		"qty": _("Qty") + ":Float:80",
-		"base_rate": _("Avg. Selling Rate") + ":Currency/currency:100",
-		"buying_rate": _("Valuation Rate") + ":Currency/currency:100",
-		"base_amount": _("Selling Amount") + ":Currency/currency:100",
-		"buying_amount": _("Buying Amount") + ":Currency/currency:100",
-		"gross_profit": _("Gross Profit") + ":Currency/currency:100",
-		"gross_profit_percent": _("Gross Profit %") + ":Percent:100",
-		"project": _("Project") + ":Link/Project:100",
-		"sales_person": _("Sales person"),
-		"allocated_amount": _("Allocated Amount") + ":Currency/currency:100",
-		"customer": _("Customer") + ":Link/Customer:100",
-		"customer_group": _("Customer Group") + ":Link/Customer Group:100",
-		"territory": _("Territory") + ":Link/Territory:100"
+		"parent": {"label": _('Sales Invoice'), "fieldname": "parent_invoice", "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
+		"invoice_or_item": {"label": _('Sales Invoice'), "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
+		"posting_date": {"label": _('Posting Date'), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
+		"posting_time": {"label": _('Posting Time'), "fieldname": "posting_time", "fieldtype": "Data", "width": 100},
+		"item_code": {"label": _('Item Code'), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100},
+		"item_name": {"label": _('Item Name'), "fieldname": "item_name", "fieldtype": "Data", "width": 100},
+		"item_group": {"label": _('Item Group'), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100},
+		"brand": {"label": _('Brand'), "fieldtype": "Link", "options": "Brand", "width": 100},
+		"description": {"label": _('Description'), "fieldname": "description",  "fieldtype": "Data", "width": 100},
+		"warehouse": {"label": _('Warehouse'), "fieldname": "warehouse", "fieldtype": "Link", "options": "warehouse", "width": 100},
+		"qty": {"label": _('Qty'), "fieldname": "qty", "fieldtype": "Float", "width": 80},
+		"base_rate": {"label": _('Avg. Selling Rate'), "fieldname": "avg._selling_rate",  "fieldtype": "Currency", "options": "currency", "width": 100},
+		"buying_rate": {"label": _('Valuation Rate'), "fieldname": "valuation_rate", "fieldtype": "Currency", "options": "currency", "width": 100},
+		"base_amount": {"label": _('Selling Amount'), "fieldname": "selling_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
+		"buying_amount": {"label": _('Buying Amount'), "fieldname": "buying_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
+		"gross_profit": {"label": _('Gross Profit'), "fieldname": "gross_profit", "fieldtype": "Currency", "options": "currency", "width": 100},
+		"gross_profit_percent": {"label": _('Gross Profit Percent'), "fieldname": "gross_profit_%",
+			"fieldtype": "Percent", "width": 100},
+		"project": {"label": _('Project'), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
+		"sales_person": {"label": _('Sales Person'), "fieldname": "sales_person", "fieldtype": "Data","width": 100},
+		"allocated_amount": {"label": _('Allocated Amount'), "fieldname": "allocated_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
+		"customer": {"label": _('Customer'), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "width": 100},
+		"customer_group": {"label": _('Customer Group'), "fieldname": "customer_group", "fieldtype": "Link", "options": "customer", "width": 100},
+		"territory": {"label": _('Territory'), "fieldname": "territory",  "fieldtype": "Link", "options": "territory", "width": 100},
 	})
 
 	for col in group_wise_columns.get(scrub(filters.group_by)):
@@ -173,7 +172,7 @@
 			buying_amount = 0
 
 		for row in reversed(self.si_list):
-			if self.skip_row(row, self.product_bundles):
+			if self.skip_row(row):
 				continue
 
 			row.base_amount = flt(row.base_net_amount, self.currency_precision)
@@ -223,16 +222,6 @@
 			self.get_average_rate_based_on_group_by()
 
 	def get_average_rate_based_on_group_by(self):
-		# sum buying / selling totals for group
-		self.totals = frappe._dict(
-			qty=0,
-			base_amount=0,
-			buying_amount=0,
-			gross_profit=0,
-			gross_profit_percent=0,
-			base_rate=0,
-			buying_rate=0
-		)
 		for key in list(self.grouped):
 			if self.filters.get("group_by") != "Invoice":
 				for i, row in enumerate(self.grouped[key]):
@@ -244,7 +233,6 @@
 						new_row.base_amount += flt(row.base_amount, self.currency_precision)
 				new_row = self.set_average_rate(new_row)
 				self.grouped_data.append(new_row)
-				self.add_to_totals(new_row)
 			else:
 				for i, row in enumerate(self.grouped[key]):
 					if row.indent == 1.0:
@@ -258,17 +246,6 @@
 						if (flt(row.qty) or row.base_amount):
 							row = self.set_average_rate(row)
 							self.grouped_data.append(row)
-						self.add_to_totals(row)
-
-		self.set_average_gross_profit(self.totals)
-
-		if self.filters.get("group_by") == "Invoice":
-			self.totals.indent = 0.0
-			self.totals.parent_invoice = ""
-			self.totals.invoice_or_item = "Total"
-			self.si_list.append(self.totals)
-		else:
-			self.grouped_data.append(self.totals)
 
 	def is_not_invoice_row(self, row):
 		return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice"
@@ -284,11 +261,6 @@
 		new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \
 			if new_row.base_amount else 0
 
-	def add_to_totals(self, new_row):
-		for key in self.totals:
-			if new_row.get(key):
-				self.totals[key] += new_row[key]
-
 	def get_returned_invoice_items(self):
 		returned_invoices = frappe.db.sql("""
 			select
@@ -306,12 +278,12 @@
 			self.returned_invoices.setdefault(inv.return_against, frappe._dict())\
 				.setdefault(inv.item_code, []).append(inv)
 
-	def skip_row(self, row, product_bundles):
+	def skip_row(self, row):
 		if self.filters.get("group_by") != "Invoice":
 			if not row.get(scrub(self.filters.get("group_by", ""))):
 				return True
-		elif row.get("is_return") == 1:
-			return True
+
+		return False
 
 	def get_buying_amount_from_product_bundle(self, row, product_bundle):
 		buying_amount = 0.0
@@ -369,20 +341,37 @@
 		return self.average_buying_rate[item_code]
 
 	def get_last_purchase_rate(self, item_code, row):
-		condition = ''
-		if row.project:
-			condition += " AND a.project=%s" % (frappe.db.escape(row.project))
-		elif row.cost_center:
-			condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center))
-		if self.filters.to_date:
-			condition += " AND modified='%s'" % (self.filters.to_date)
+		purchase_invoice = frappe.qb.DocType("Purchase Invoice")
+		purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
 
-		last_purchase_rate = frappe.db.sql("""
-		select (a.base_rate / a.conversion_factor)
-		from `tabPurchase Invoice Item` a
-		where a.item_code = %s and a.docstatus=1
-		{0}
-		order by a.modified desc limit 1""".format(condition), item_code)
+		query = (frappe.qb.from_(purchase_invoice_item)
+			.inner_join(
+				purchase_invoice
+			).on(
+				purchase_invoice.name == purchase_invoice_item.parent
+			).select(
+				purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor
+			).where(
+				purchase_invoice.docstatus == 1
+			).where(
+				purchase_invoice.posting_date <= self.filters.to_date
+			).where(
+				purchase_invoice_item.item_code == item_code
+			))
+
+		if row.project:
+			query.where(
+				purchase_invoice_item.project == row.project
+			)
+
+		if row.cost_center:
+			query.where(
+				purchase_invoice_item.cost_center == row.cost_center
+			)
+
+		query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc)
+		query.limit(1)
+		last_purchase_rate = query.run()
 
 		return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
 
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index 57f7974..e6cbff5 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -43,7 +43,7 @@
 			if entry.account in tds_accounts:
 				tds_deducted += (entry.credit - entry.debit)
 
-			total_amount_credited += (entry.credit - entry.debit)
+			total_amount_credited += entry.credit
 
 		if tds_deducted:
 			row = {
diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py
index 78c109a..4ed966d 100644
--- a/erpnext/accounts/test/test_reports.py
+++ b/erpnext/accounts/test/test_reports.py
@@ -39,10 +39,11 @@
 	def test_execute_all_accounts_reports(self):
 		"""Test that all script report in stock modules are executable with supported filters"""
 		for report, filter in REPORT_FILTER_TEST_CASES:
-			execute_script_report(
-				report_name=report,
-				module="Accounts",
-				filters=filter,
-				default_filters=DEFAULT_FILTERS,
-				optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
-			)
+			with self.subTest(report=report):
+				execute_script_report(
+					report_name=report,
+					module="Accounts",
+					filters=filter,
+					default_filters=DEFAULT_FILTERS,
+					optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+				)
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 6e87426..ea473fa 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -417,11 +417,12 @@
 	def validate_asset_finance_books(self, row):
 		if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
 			frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
-				.format(row.idx))
+				.format(row.idx), title=_("Invalid Schedule"))
 
 		if not row.depreciation_start_date:
 			if not self.available_for_use_date:
-				frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
+				frappe.throw(_("Row {0}: Depreciation Start Date is required")
+					.format(row.idx), title=_("Invalid Schedule"))
 			row.depreciation_start_date = get_last_day(self.available_for_use_date)
 
 		if not self.is_existing_asset:
@@ -439,8 +440,9 @@
 			else:
 				self.number_of_depreciations_booked = 0
 
-			if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
-				frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
+			if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
+				frappe.throw(_("Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked")
+					.format(row.idx), title=_("Invalid Schedule"))
 
 		if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
 			frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index c08dc21..ddbff89 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -873,8 +873,9 @@
 		self.assertRaises(frappe.ValidationError, asset.save)
 
 	def test_number_of_depreciations(self):
-		"""Tests if an error is raised when number_of_depreciations_booked > total_number_of_depreciations."""
+		"""Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
 
+		# number_of_depreciations_booked > total_number_of_depreciations
 		asset = create_asset(
 			item_code = "Macbook Pro",
 			calculate_depreciation = 1,
@@ -889,6 +890,21 @@
 
 		self.assertRaises(frappe.ValidationError, asset.save)
 
+		# number_of_depreciations_booked = total_number_of_depreciations
+		asset_2 = create_asset(
+			item_code = "Macbook Pro",
+			calculate_depreciation = 1,
+			available_for_use_date = "2019-12-31",
+			total_number_of_depreciations = 5,
+			expected_value_after_useful_life = 10000,
+			depreciation_start_date = "2020-07-01",
+			opening_accumulated_depreciation = 10000,
+			number_of_depreciations_booked = 5,
+			do_not_save = 1
+		)
+
+		self.assertRaises(frappe.ValidationError, asset_2.save)
+
 	def test_depreciation_start_date_is_before_purchase_date(self):
 		asset = create_asset(
 			item_code = "Macbook Pro",
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index a181af7..b740476 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -249,6 +249,7 @@
 						"posting_time": self.get('posting_time'),
 						"qty": -1 * flt(d.get('stock_qty')),
 						"serial_no": d.get('serial_no'),
+						"batch_no": d.get("batch_no"),
 						"company": self.company,
 						"voucher_type": self.doctype,
 						"voucher_no": self.name,
@@ -278,7 +279,8 @@
 						"posting_date": self.posting_date,
 						"posting_time": self.posting_time,
 						"qty": -1 * d.consumed_qty,
-						"serial_no": d.serial_no
+						"serial_no": d.serial_no,
+						"batch_no": d.batch_no,
 					})
 
 					if rate > 0:
diff --git a/erpnext/controllers/employee_boarding_controller.py b/erpnext/controllers/employee_boarding_controller.py
index ae2c737..dd02ce1 100644
--- a/erpnext/controllers/employee_boarding_controller.py
+++ b/erpnext/controllers/employee_boarding_controller.py
@@ -104,11 +104,11 @@
 	def get_task_dates(self, activity, holiday_list):
 		start_date = end_date = None
 
-		if activity.begin_on:
+		if activity.begin_on is not None:
 			start_date = add_days(self.boarding_begins_on, activity.begin_on)
 			start_date = self.update_if_holiday(start_date, holiday_list)
 
-			if activity.duration:
+			if activity.duration is not None:
 				end_date = add_days(self.boarding_begins_on, activity.begin_on + activity.duration)
 				end_date = self.update_if_holiday(end_date, holiday_list)
 
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index df3c5f1..8c3aab4 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -420,6 +420,7 @@
 				"posting_time": sle.get('posting_time'),
 				"qty": sle.actual_qty,
 				"serial_no": sle.get('serial_no'),
+				"batch_no": sle.get("batch_no"),
 				"company": sle.company,
 				"voucher_type": sle.voucher_type,
 				"voucher_no": sle.voucher_no
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 31b2209..e918cde 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -394,6 +394,7 @@
 						"posting_time": self.get('posting_time') or nowtime(),
 						"qty": qty if cint(self.get("is_return")) else (-1 * qty),
 						"serial_no": d.get('serial_no'),
+						"batch_no": d.get("batch_no"),
 						"company": self.company,
 						"voucher_type": self.doctype,
 						"voucher_no": self.name,
diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py
index a4e2157..14c86d5 100644
--- a/erpnext/erpnext_integrations/taxjar_integration.py
+++ b/erpnext/erpnext_integrations/taxjar_integration.py
@@ -8,10 +8,6 @@
 
 from erpnext import get_default_company, get_region
 
-TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
-SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
-TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
-TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
 SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
 	"FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO",
 	"SE", "SI", "SK", "US"]
@@ -35,12 +31,14 @@
 	if api_key and api_url:
 		client = taxjar.Client(api_key=api_key, api_url=api_url)
 		client.set_api_config('headers', {
-				'x-api-version': '2020-08-07'
+				'x-api-version': '2022-01-24'
 			})
 		return client
 
 
 def create_transaction(doc, method):
+	TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
+
 	"""Create an order transaction in TaxJar"""
 
 	if not TAXJAR_CREATE_TRANSACTIONS:
@@ -51,6 +49,7 @@
 	if not client:
 		return
 
+	TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
 	sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD])
 
 	if not sales_tax:
@@ -79,6 +78,7 @@
 
 def delete_transaction(doc, method):
 	"""Delete an existing TaxJar order transaction"""
+	TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
 
 	if not TAXJAR_CREATE_TRANSACTIONS:
 		return
@@ -92,6 +92,8 @@
 
 
 def get_tax_data(doc):
+	SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
+
 	from_address = get_company_address_details(doc)
 	from_shipping_state = from_address.get("state")
 	from_country_code = frappe.db.get_value("Country", from_address.country, "code")
@@ -113,20 +115,20 @@
 		to_shipping_state = get_state_code(to_address, 'Shipping')
 
 	tax_dict = {
-		'from_country': from_country_code,
-		'from_zip': from_address.pincode,
-		'from_state': from_shipping_state,
-		'from_city': from_address.city,
-		'from_street': from_address.address_line1,
-		'to_country': to_country_code,
-		'to_zip': to_address.pincode,
-		'to_city': to_address.city,
-		'to_street': to_address.address_line1,
-		'to_state': to_shipping_state,
-		'shipping': shipping,
-		'amount': doc.net_total,
-		'plugin': 'erpnext',
-		'line_items': line_items
+		"from_country": from_country_code,
+		"from_zip": from_address.pincode,
+		"from_state": from_shipping_state,
+		"from_city": from_address.city,
+		"from_street": from_address.address_line1,
+		"to_country": to_country_code,
+		"to_zip": to_address.pincode,
+		"to_city": to_address.city,
+		"to_street": to_address.address_line1,
+		"to_state": to_shipping_state,
+		"shipping": shipping,
+		"amount": doc.net_total,
+		"plugin": "erpnext",
+		"line_items": line_items
 	}
 	return tax_dict
 
@@ -156,6 +158,9 @@
 	return tax_dict
 
 def set_sales_tax(doc, method):
+	TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
+	TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
+
 	if not TAXJAR_CALCULATE_TAX:
 		return
 
@@ -206,6 +211,7 @@
 			doc.run_method("calculate_taxes_and_totals")
 
 def check_for_nexus(doc, tax_dict):
+	TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
 	if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
 		for item in doc.get("items"):
 			item.tax_collectable = flt(0)
@@ -218,6 +224,8 @@
 
 def check_sales_tax_exemption(doc):
 	# if the party is exempt from sales tax, then set all tax account heads to zero
+	TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
+
 	sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
 		or frappe.db.has_column("Customer", "exempt_from_sales_tax") \
 		and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index 2d129c8..0fb821d 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -4,7 +4,7 @@
 import unittest
 
 import frappe
-from frappe.utils import getdate
+from frappe.utils import add_days, getdate
 
 from erpnext.hr.doctype.employee_onboarding.employee_onboarding import (
 	IncompleteTaskError,
@@ -35,6 +35,15 @@
 		# boarding status
 		self.assertEqual(onboarding.boarding_status, 'Pending')
 
+		# start and end dates
+		start_date, end_date = frappe.db.get_value('Task', onboarding.activities[0].task, ['exp_start_date', 'exp_end_date'])
+		self.assertEqual(getdate(start_date), getdate(onboarding.boarding_begins_on))
+		self.assertEqual(getdate(end_date), add_days(start_date, onboarding.activities[0].duration))
+
+		start_date, end_date = frappe.db.get_value('Task', onboarding.activities[1].task, ['exp_start_date', 'exp_end_date'])
+		self.assertEqual(getdate(start_date), add_days(onboarding.boarding_begins_on, onboarding.activities[0].duration))
+		self.assertEqual(getdate(end_date), add_days(start_date, onboarding.activities[1].duration))
+
 		# complete the task
 		project = frappe.get_doc('Project', onboarding.project)
 		for task in frappe.get_all('Task', dict(project=project.name)):
@@ -57,10 +66,7 @@
 		self.assertEqual(employee.employee_name, 'Test Researcher')
 
 	def tearDown(self):
-		for entry in frappe.get_all('Employee Onboarding'):
-			doc = frappe.get_doc('Employee Onboarding', entry.name)
-			doc.cancel()
-			doc.delete()
+		frappe.db.rollback()
 
 
 def get_job_applicant():
@@ -87,23 +93,31 @@
 def create_employee_onboarding():
 	applicant = get_job_applicant()
 	job_offer = get_job_offer(applicant.name)
-	holiday_list = make_holiday_list()
+
+	holiday_list = make_holiday_list('_Test Employee Boarding')
+	holiday_list = frappe.get_doc('Holiday List', holiday_list)
+	holiday_list.holidays = []
+	holiday_list.save()
 
 	onboarding = frappe.new_doc('Employee Onboarding')
 	onboarding.job_applicant = applicant.name
 	onboarding.job_offer = job_offer.name
 	onboarding.date_of_joining = onboarding.boarding_begins_on = getdate()
 	onboarding.company = '_Test Company'
-	onboarding.holiday_list = holiday_list
+	onboarding.holiday_list = holiday_list.name
 	onboarding.designation = 'Researcher'
 	onboarding.append('activities', {
 		'activity_name': 'Assign ID Card',
 		'role': 'HR User',
-		'required_for_employee_creation': 1
+		'required_for_employee_creation': 1,
+		'begin_on': 0,
+		'duration': 1
 	})
 	onboarding.append('activities', {
 		'activity_name': 'Assign a laptop',
-		'role': 'HR User'
+		'role': 'HR User',
+		'begin_on': 1,
+		'duration': 1
 	})
 	onboarding.status = 'Pending'
 	onboarding.insert()
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
index 7811d56..50926d7 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
@@ -14,11 +14,15 @@
   "applicant",
   "section_break_7",
   "disbursement_date",
+  "clearance_date",
   "column_break_8",
   "disbursed_amount",
   "accounting_dimensions_section",
   "cost_center",
-  "customer_details_section",
+  "accounting_details",
+  "disbursement_account",
+  "column_break_16",
+  "loan_account",
   "bank_account",
   "disbursement_references_section",
   "reference_date",
@@ -107,11 +111,6 @@
    "label": "Disbursement Details"
   },
   {
-   "fieldname": "customer_details_section",
-   "fieldtype": "Section Break",
-   "label": "Customer Details"
-  },
-  {
    "fetch_from": "against_loan.applicant_type",
    "fieldname": "applicant_type",
    "fieldtype": "Select",
@@ -149,15 +148,48 @@
    "fieldname": "reference_number",
    "fieldtype": "Data",
    "label": "Reference Number"
+  },
+  {
+   "fieldname": "clearance_date",
+   "fieldtype": "Date",
+   "label": "Clearance Date",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "accounting_details",
+   "fieldtype": "Section Break",
+   "label": "Accounting Details"
+  },
+  {
+   "fetch_from": "against_loan.disbursement_account",
+   "fieldname": "disbursement_account",
+   "fieldtype": "Link",
+   "label": "Disbursement Account",
+   "options": "Account",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_16",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fetch_from": "against_loan.loan_account",
+   "fieldname": "loan_account",
+   "fieldtype": "Link",
+   "label": "Loan Account",
+   "options": "Account",
+   "read_only": 1
   }
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-04-19 18:09:32.175355",
+ "modified": "2022-02-17 18:23:44.157598",
  "modified_by": "Administrator",
  "module": "Loan Management",
  "name": "Loan Disbursement",
+ "naming_rule": "Expression (old style)",
  "owner": "Administrator",
  "permissions": [
   {
@@ -194,5 +226,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index df3aadf..54a03b9 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -42,9 +42,6 @@
 		if not self.posting_date:
 			self.posting_date = self.disbursement_date or nowdate()
 
-		if not self.bank_account and self.applicant_type == "Customer":
-			self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account")
-
 	def validate_disbursal_amount(self):
 		possible_disbursal_amount = get_disbursal_amount(self.against_loan)
 
@@ -117,12 +114,11 @@
 
 	def make_gl_entries(self, cancel=0, adv_adj=0):
 		gle_map = []
-		loan_details = frappe.get_doc("Loan", self.against_loan)
 
 		gle_map.append(
 			self.get_gl_dict({
-				"account": loan_details.loan_account,
-				"against": loan_details.disbursement_account,
+				"account": self.loan_account,
+				"against": self.disbursement_account,
 				"debit": self.disbursed_amount,
 				"debit_in_account_currency": self.disbursed_amount,
 				"against_voucher_type": "Loan",
@@ -137,8 +133,8 @@
 
 		gle_map.append(
 			self.get_gl_dict({
-				"account": loan_details.disbursement_account,
-				"against": loan_details.loan_account,
+				"account": self.disbursement_account,
+				"against": self.loan_account,
 				"credit": self.disbursed_amount,
 				"credit_in_account_currency": self.disbursed_amount,
 				"against_voucher_type": "Loan",
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index 0de073f..1c800a0 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -74,39 +74,6 @@
 				})
 			)
 
-		if self.payable_principal_amount:
-			gle_map.append(
-				self.get_gl_dict({
-					"account": self.loan_account,
-					"party_type": self.applicant_type,
-					"party": self.applicant,
-					"against": self.interest_income_account,
-					"debit": self.payable_principal_amount,
-					"debit_in_account_currency": self.interest_amount,
-					"against_voucher_type": "Loan",
-					"against_voucher": self.loan,
-					"remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
-						self.last_accrual_date, self.posting_date, self.loan),
-					"cost_center": erpnext.get_default_cost_center(self.company),
-					"posting_date": self.posting_date
-				})
-			)
-
-			gle_map.append(
-				self.get_gl_dict({
-					"account": self.interest_income_account,
-					"against": self.loan_account,
-					"credit": self.payable_principal_amount,
-					"credit_in_account_currency":  self.interest_amount,
-					"against_voucher_type": "Loan",
-					"against_voucher": self.loan,
-					"remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
-						self.last_accrual_date, self.posting_date, self.loan),
-					"cost_center": erpnext.get_default_cost_center(self.company),
-					"posting_date": self.posting_date
-				})
-			)
-
 		if gle_map:
 			make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
 
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
index 93ef217..480e010 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
@@ -1,7 +1,7 @@
 {
  "actions": [],
  "autoname": "LM-REP-.####",
- "creation": "2019-09-03 14:44:39.977266",
+ "creation": "2022-01-25 10:30:02.767941",
  "doctype": "DocType",
  "editable_grid": 1,
  "engine": "InnoDB",
@@ -13,6 +13,7 @@
   "column_break_3",
   "company",
   "posting_date",
+  "clearance_date",
   "rate_of_interest",
   "payroll_payable_account",
   "is_term_loan",
@@ -37,7 +38,12 @@
   "total_penalty_paid",
   "total_interest_paid",
   "repayment_details",
-  "amended_from"
+  "amended_from",
+  "accounting_details_section",
+  "payment_account",
+  "penalty_income_account",
+  "column_break_36",
+  "loan_account"
  ],
  "fields": [
   {
@@ -260,12 +266,52 @@
    "fieldname": "repay_from_salary",
    "fieldtype": "Check",
    "label": "Repay From Salary"
+  },
+  {
+   "fieldname": "clearance_date",
+   "fieldtype": "Date",
+   "label": "Clearance Date",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "accounting_details_section",
+   "fieldtype": "Section Break",
+   "label": "Accounting Details"
+  },
+  {
+   "fetch_from": "against_loan.payment_account",
+   "fieldname": "payment_account",
+   "fieldtype": "Link",
+   "label": "Repayment Account",
+   "options": "Account",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_36",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fetch_from": "against_loan.loan_account",
+   "fieldname": "loan_account",
+   "fieldtype": "Link",
+   "label": "Loan Account",
+   "options": "Account",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "against_loan.penalty_income_account",
+   "fieldname": "penalty_income_account",
+   "fieldtype": "Link",
+   "hidden": 1,
+   "label": "Penalty Income Account",
+   "options": "Account"
   }
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-01-06 01:51:06.707782",
+ "modified": "2022-02-18 19:10:07.742298",
  "modified_by": "Administrator",
  "module": "Loan Management",
  "name": "Loan Repayment",
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index f3ed611..67c2b1e 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -310,7 +310,6 @@
 
 	def make_gl_entries(self, cancel=0, adv_adj=0):
 		gle_map = []
-		loan_details = frappe.get_doc("Loan", self.against_loan)
 
 		if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
 			remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount,
@@ -323,13 +322,13 @@
 		if self.repay_from_salary:
 			payment_account = self.payroll_payable_account
 		else:
-			payment_account = loan_details.payment_account
+			payment_account = self.payment_account
 
 		if self.total_penalty_paid:
 			gle_map.append(
 				self.get_gl_dict({
-					"account": loan_details.loan_account,
-					"against": loan_details.payment_account,
+					"account": self.loan_account,
+					"against": payment_account,
 					"debit": self.total_penalty_paid,
 					"debit_in_account_currency": self.total_penalty_paid,
 					"against_voucher_type": "Loan",
@@ -344,8 +343,8 @@
 
 			gle_map.append(
 				self.get_gl_dict({
-					"account": loan_details.penalty_income_account,
-					"against": loan_details.loan_account,
+					"account": self.penalty_income_account,
+					"against": self.loan_account,
 					"credit": self.total_penalty_paid,
 					"credit_in_account_currency": self.total_penalty_paid,
 					"against_voucher_type": "Loan",
@@ -359,8 +358,7 @@
 		gle_map.append(
 			self.get_gl_dict({
 				"account": payment_account,
-				"against": loan_details.loan_account + ", " + loan_details.interest_income_account
-						+ ", " + loan_details.penalty_income_account,
+				"against": self.loan_account + ", " + self.penalty_income_account,
 				"debit": self.amount_paid,
 				"debit_in_account_currency": self.amount_paid,
 				"against_voucher_type": "Loan",
@@ -368,16 +366,16 @@
 				"remarks": remarks,
 				"cost_center": self.cost_center,
 				"posting_date": getdate(self.posting_date),
-				"party_type": loan_details.applicant_type if self.repay_from_salary else '',
-				"party": loan_details.applicant if self.repay_from_salary else ''
+				"party_type": self.applicant_type if self.repay_from_salary else '',
+				"party": self.applicant if self.repay_from_salary else ''
 			})
 		)
 
 		gle_map.append(
 			self.get_gl_dict({
-				"account": loan_details.loan_account,
-				"party_type": loan_details.applicant_type,
-				"party": loan_details.applicant,
+				"account": self.loan_account,
+				"party_type": self.applicant_type,
+				"party": self.applicant,
 				"against": payment_account,
 				"credit": self.amount_paid,
 				"credit_in_account_currency": self.amount_paid,
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 8d00019..9f4ace2 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -62,7 +62,7 @@
 
 		if self.get('time_logs'):
 			for d in self.get('time_logs'):
-				if get_datetime(d.from_time) > get_datetime(d.to_time):
+				if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
 					frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
 
 				data = self.get_overlap_for(d)
diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py
index 9f51ded..e436fdc 100644
--- a/erpnext/manufacturing/report/test_reports.py
+++ b/erpnext/manufacturing/report/test_reports.py
@@ -55,10 +55,11 @@
 	def test_execute_all_manufacturing_reports(self):
 		"""Test that all script report in manufacturing modules are executable with supported filters"""
 		for report, filter in REPORT_FILTER_TEST_CASES:
-			execute_script_report(
-				report_name=report,
-				module="Manufacturing",
-				filters=filter,
-				default_filters=DEFAULT_FILTERS,
-				optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
-			)
+			with self.subTest(report=report):
+				execute_script_report(
+					report_name=report,
+					module="Manufacturing",
+					filters=filter,
+					default_filters=DEFAULT_FILTERS,
+					optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+				)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index a93ceca..7560f2f 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -353,3 +353,5 @@
 erpnext.patches.v13_0.update_exchange_rate_settings
 erpnext.patches.v14_0.delete_amazon_mws_doctype
 erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
+erpnext.patches.v13_0.update_accounts_in_loan_docs
+erpnext.patches.v14_0.update_batch_valuation_flag
diff --git a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py
new file mode 100644
index 0000000..440f912
--- /dev/null
+++ b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py
@@ -0,0 +1,37 @@
+import frappe
+
+
+def execute():
+	ld = frappe.qb.DocType("Loan Disbursement").as_("ld")
+	lr = frappe.qb.DocType("Loan Repayment").as_("lr")
+	loan = frappe.qb.DocType("Loan")
+
+	frappe.qb.update(
+		ld
+	).inner_join(
+		loan
+	).on(
+		loan.name == ld.against_loan
+	).set(
+		ld.disbursement_account, loan.disbursement_account
+	).set(
+		ld.loan_account, loan.loan_account
+	).where(
+		ld.docstatus < 2
+	).run()
+
+	frappe.qb.update(
+		lr
+	).inner_join(
+		loan
+	).on(
+		loan.name == lr.against_loan
+	).set(
+		lr.payment_account, loan.payment_account
+	).set(
+		lr.loan_account, loan.loan_account
+	).set(
+		lr.penalty_income_account, loan.penalty_income_account
+	).where(
+		lr.docstatus < 2
+	).run()
diff --git a/erpnext/patches/v14_0/update_batch_valuation_flag.py b/erpnext/patches/v14_0/update_batch_valuation_flag.py
new file mode 100644
index 0000000..55c8c48
--- /dev/null
+++ b/erpnext/patches/v14_0/update_batch_valuation_flag.py
@@ -0,0 +1,11 @@
+import frappe
+
+
+def execute():
+	"""
+	- Don't use batchwise valuation for existing batches.
+	- Only batches created after this patch shoule use it.
+	"""
+
+	batch = frappe.qb.DocType("Batch")
+	frappe.qb.update(batch).set(batch.use_batchwise_valuation, 0).run()
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index f727ff4..d2a3998 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -1268,7 +1268,7 @@
 			for i, earning in enumerate(self.earnings):
 				if earning.salary_component == salary_component:
 					self.earnings[i].amount = wages_amount
-				self.gross_pay += self.earnings[i].amount
+				self.gross_pay += flt(self.earnings[i].amount, earning.precision("amount"))
 		self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
 
 	def compute_year_to_date(self):
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index daa0f89..6a5debf 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -1019,13 +1019,13 @@
 	frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None)
 	frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None)
 
-def make_holiday_list():
+def make_holiday_list(holiday_list_name=None):
 	fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
-	holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List")
+	holiday_list = frappe.db.exists("Holiday List", holiday_list_name or "Salary Slip Test Holiday List")
 	if not holiday_list:
 		holiday_list = frappe.get_doc({
 			"doctype": "Holiday List",
-			"holiday_list_name": "Salary Slip Test Holiday List",
+			"holiday_list_name": holiday_list_name or "Salary Slip Test Holiday List",
 			"from_date": fiscal_year[1],
 			"to_date": fiscal_year[2],
 			"weekly_off": "Sunday"
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index ca73393..214a1be 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -182,6 +182,12 @@
 				onchange: () => this.update_options(),
 			},
 			{
+				fieldtype: "Check",
+				label: "Loan Repayment",
+				fieldname: "loan_repayment",
+				onchange: () => this.update_options(),
+			},
+			{
 				fieldname: "column_break_5",
 				fieldtype: "Column Break",
 			},
@@ -191,7 +197,6 @@
 				fieldname: "sales_invoice",
 				onchange: () => this.update_options(),
 			},
-
 			{
 				fieldtype: "Check",
 				label: "Purchase Invoice",
@@ -199,6 +204,12 @@
 				onchange: () => this.update_options(),
 			},
 			{
+				fieldtype: "Check",
+				label: "Show Only Exact Amount",
+				fieldname: "exact_match",
+				onchange: () => this.update_options(),
+			},
+			{
 				fieldname: "column_break_5",
 				fieldtype: "Column Break",
 			},
@@ -210,8 +221,8 @@
 			},
 			{
 				fieldtype: "Check",
-				label: "Show Only Exact Amount",
-				fieldname: "exact_match",
+				label: "Loan Disbursement",
+				fieldname: "loan_disbursement",
 				onchange: () => this.update_options(),
 			},
 			{
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 136e1ed..ae8c0c8 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -525,6 +525,7 @@
 
 		item.weight_per_unit = 0;
 		item.weight_uom = '';
+		item.conversion_factor = 0;
 
 		if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
 			update_stock = cint(me.frm.doc.update_stock);
@@ -719,6 +720,7 @@
 			'posting_time': posting_time,
 			'qty': item.qty * item.conversion_factor,
 			'serial_no': item.serial_no,
+			'batch_no': item.batch_no,
 			'voucher_type': voucher_type,
 			'company': company,
 			'allow_zero_valuation_rate': item.allow_zero_valuation_rate
diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js
index 4b98978..9999a6d 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.js
+++ b/erpnext/regional/report/gstr_1/gstr_1.js
@@ -17,7 +17,7 @@
 			"fieldtype": "Link",
 			"options": "Address",
 			"get_query": function () {
-				var company = frappe.query_report.get_filter_value('company');
+				let company = frappe.query_report.get_filter_value('company');
 				if (company) {
 					return {
 						"query": 'frappe.contacts.doctype.address.address.address_query',
@@ -27,6 +27,11 @@
 			}
 		},
 		{
+			"fieldname": "company_gstin",
+			"label": __("Company GSTIN"),
+			"fieldtype": "Select"
+		},
+		{
 			"fieldname": "from_date",
 			"label": __("From Date"),
 			"fieldtype": "Date",
@@ -60,10 +65,21 @@
 		}
 	],
 	onload: function (report) {
+		let filters = report.get_values();
+
+		frappe.call({
+			method: 'erpnext.regional.report.gstr_1.gstr_1.get_company_gstins',
+			args: {
+				company: filters.company
+			},
+			callback: function(r) {
+				frappe.query_report.page.fields_dict.company_gstin.df.options = r.message;
+				frappe.query_report.page.fields_dict.company_gstin.refresh();
+			}
+		});
+
 
 		report.page.add_inner_button(__("Download as JSON"), function () {
-			var filters = report.get_values();
-
 			frappe.call({
 				method: 'erpnext.regional.report.gstr_1.gstr_1.get_json',
 				args: {
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index ce2ffb4..8fcb6bb 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -253,7 +253,8 @@
 		for opts in (("company", " and company=%(company)s"),
 			("from_date", " and posting_date>=%(from_date)s"),
 			("to_date", " and posting_date<=%(to_date)s"),
-			("company_address", " and company_address=%(company_address)s")):
+			("company_address", " and company_address=%(company_address)s"),
+			("company_gstin", " and company_gstin=%(company_gstin)s")):
 				if self.filters.get(opts[0]):
 					conditions += opts[1]
 
@@ -1192,3 +1193,23 @@
 		return True
 	else:
 		return False
+
+
+@frappe.whitelist()
+def get_company_gstins(company):
+	address = frappe.qb.DocType("Address")
+	links = frappe.qb.DocType("Dynamic Link")
+
+	addresses = frappe.qb.from_(address).inner_join(links).on(
+		address.name == links.parent
+	).select(
+		address.gstin
+	).where(
+		links.link_doctype == 'Company'
+	).where(
+		links.link_name == company
+	).run(as_dict=1)
+
+	address_list = [''] + [d.gstin for d in addresses]
+
+	return address_list
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index 95f6c4e..080d517 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -83,8 +83,8 @@
   "planned_qty",
   "column_break_69",
   "work_order_qty",
-  "delivered_qty",
   "produced_qty",
+  "delivered_qty",
   "returned_qty",
   "shopping_cart_section",
   "additional_notes",
@@ -701,10 +701,8 @@
    "width": "50px"
   },
   {
-   "description": "For Production",
    "fieldname": "produced_qty",
    "fieldtype": "Float",
-   "hidden": 1,
    "label": "Produced Quantity",
    "oldfieldname": "produced_qty",
    "oldfieldtype": "Currency",
@@ -802,7 +800,7 @@
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-10-05 12:27:25.014789",
+ "modified": "2022-02-21 13:55:08.883104",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Sales Order Item",
@@ -811,5 +809,6 @@
  "permissions": [],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index 9650bc8..4d75e6e 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -180,14 +180,6 @@
 						() => frm.save(),
 						() => this.update_totals_section(frm.doc)
 					]);
-				} else {
-					frappe.run_serially([
-						() => frm.doc.ignore_pricing_rule=1,
-						() => frm.trigger('ignore_pricing_rule'),
-						() => frm.doc.ignore_pricing_rule=0,
-						() => frm.save(),
-						() => this.update_totals_section(frm.doc)
-					]);
 				}
 			}
 		});
diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json
index fc4cf1d..967c572 100644
--- a/erpnext/stock/doctype/batch/batch.json
+++ b/erpnext/stock/doctype/batch/batch.json
@@ -9,6 +9,8 @@
  "field_order": [
   "sb_disabled",
   "disabled",
+  "column_break_24",
+  "use_batchwise_valuation",
   "sb_batch",
   "batch_id",
   "item",
@@ -186,6 +188,18 @@
    "fieldtype": "Float",
    "label": "Produced Qty",
    "read_only": 1
+  },
+  {
+   "fieldname": "column_break_24",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "use_batchwise_valuation",
+   "fieldtype": "Check",
+   "label": "Use Batch-wise Valuation",
+   "read_only": 1,
+   "set_only_once": 1
   }
  ],
  "icon": "fa fa-archive",
@@ -193,10 +207,11 @@
  "image_field": "image",
  "links": [],
  "max_attachments": 5,
- "modified": "2021-07-08 16:22:01.343105",
+ "modified": "2022-02-21 08:08:23.999236",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Batch",
+ "naming_rule": "By fieldname",
  "owner": "Administrator",
  "permissions": [
   {
@@ -217,6 +232,7 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "title_field": "batch_id",
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 96751d6..c9b4c14 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -110,11 +110,18 @@
 
 	def validate(self):
 		self.item_has_batch_enabled()
+		self.set_batchwise_valuation()
 
 	def item_has_batch_enabled(self):
 		if frappe.db.get_value("Item", self.item, "has_batch_no") == 0:
 			frappe.throw(_("The selected item cannot have Batch"))
 
+	def set_batchwise_valuation(self):
+		from erpnext.stock.stock_ledger import get_valuation_method
+
+		if self.is_new() and get_valuation_method(self.item) != "Moving Average":
+			self.use_batchwise_valuation = 1
+
 	def before_save(self):
 		has_expiry_date, shelf_life_in_days = frappe.db.get_value('Item', self.item, ['has_expiry_date', 'shelf_life_in_days'])
 		if not self.expiry_date and has_expiry_date and shelf_life_in_days:
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 0a663c2..baa0302 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -1,13 +1,21 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
+import json
+
 import frappe
 from frappe.exceptions import ValidationError
 from frappe.utils import cint, flt
+from frappe.utils.data import add_to_date, getdate
 
 from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
 from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+	create_stock_reconciliation,
+)
 from erpnext.stock.get_item_details import get_item_details
+from erpnext.stock.stock_ledger import get_valuation_rate
 from erpnext.tests.utils import ERPNextTestCase
 
 
@@ -300,6 +308,105 @@
 		details = get_item_details(args)
 		self.assertEqual(details.get('price_list_rate'), 400)
 
+
+	def test_basic_batch_wise_valuation(self, batch_qty = 100):
+		item_code = "_TestBatchWiseVal"
+		warehouse = "_Test Warehouse - _TC"
+		self.make_batch_item(item_code)
+
+		rates = [42, 420]
+
+		batches = {}
+		for rate in rates:
+			se = make_stock_entry(item_code=item_code, qty=10, rate=rate, target=warehouse)
+			batches[se.items[0].batch_no] = rate
+
+		LOW, HIGH = list(batches.keys())
+
+		# consume things out of order
+		consumption_plan = [
+			(HIGH, 1),
+			(LOW, 2),
+			(HIGH, 2),
+			(HIGH, 4),
+			(LOW, 6),
+		]
+
+		stock_value = sum(rates) * 10
+		qty_after_transaction = 20
+		for batch, qty in consumption_plan:
+			# consume out of order
+			se = make_stock_entry(item_code=item_code, source=warehouse, qty=qty, batch_no=batch)
+
+			sle = frappe.get_last_doc("Stock Ledger Entry", {"is_cancelled": 0, "voucher_no": se.name})
+
+			stock_value_difference = sle.actual_qty * batches[sle.batch_no]
+			self.assertAlmostEqual(sle.stock_value_difference, stock_value_difference)
+
+			stock_value += stock_value_difference
+			self.assertAlmostEqual(sle.stock_value, stock_value)
+
+			qty_after_transaction += sle.actual_qty
+			self.assertAlmostEqual(sle.qty_after_transaction, qty_after_transaction)
+			self.assertAlmostEqual(sle.valuation_rate, stock_value / qty_after_transaction)
+
+			self.assertEqual(json.loads(sle.stock_queue), [])  # queues don't apply on batched items
+
+	def test_moving_batch_valuation_rates(self):
+		item_code = "_TestBatchWiseVal"
+		warehouse = "_Test Warehouse - _TC"
+		self.make_batch_item(item_code)
+
+		def assertValuation(expected):
+			actual = get_valuation_rate(item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no)
+			self.assertAlmostEqual(actual, expected)
+
+		se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse)
+		batch_no = se.items[0].batch_no
+		assertValuation(10)
+
+		# consumption should never affect current valuation rate
+		make_stock_entry(item_code=item_code, qty=20, source=warehouse)
+		assertValuation(10)
+
+		make_stock_entry(item_code=item_code, qty=30, source=warehouse)
+		assertValuation(10)
+
+		# 50 * 10 = 500 current value, add more item with higher valuation
+		make_stock_entry(item_code=item_code, qty=50, rate=20, target=warehouse, batch_no=batch_no)
+		assertValuation(15)
+
+		# consuming again shouldn't do anything
+		make_stock_entry(item_code=item_code, qty=20, source=warehouse)
+		assertValuation(15)
+
+		# reset rate with stock reconiliation
+		create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=10, rate=25, batch_no=batch_no)
+		assertValuation(25)
+
+		make_stock_entry(item_code=item_code, qty=20, rate=20, target=warehouse, batch_no=batch_no)
+		assertValuation((20 * 20 + 10 * 25) / (10 + 20))
+
+
+	def test_update_batch_properties(self):
+		item_code = "_TestBatchWiseVal"
+		self.make_batch_item(item_code)
+
+		se = make_stock_entry(item_code=item_code, qty=100, rate=10, target="_Test Warehouse - _TC")
+		batch_no = se.items[0].batch_no
+		batch = frappe.get_doc("Batch", batch_no)
+
+		expiry_date = add_to_date(batch.manufacturing_date, days=30)
+
+		batch.expiry_date = expiry_date
+		batch.save()
+
+		batch.reload()
+
+		self.assertEqual(getdate(batch.expiry_date), getdate(expiry_date))
+
+
+
 def create_batch(item_code, rate, create_item_price_for_batch):
 	pi = make_purchase_invoice(company="_Test Company",
 		warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1,
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index dfc0918..ffea9c2 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -594,7 +594,7 @@
 							const increment = r.message.increment;
 
 							let values = [];
-							for(var i = from; i <= to; i += increment) {
+							for(var i = from; i <= to; i = flt(i + increment, 6)) {
 								values.push(i);
 							}
 							attr_val_fields[d.attribute] = values;
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index b39328f..51209ac 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -56,14 +56,13 @@
 				if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty):
 					frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no))
 
-	# Validate
-	# ---------------------
 	def validate(self):
 		super(MaterialRequest, self).validate()
 
 		self.validate_schedule_date()
 		self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order')
 		self.validate_uom_is_integer("uom", "qty")
+		self.validate_material_request_type()
 
 		if not self.status:
 			self.status = "Draft"
@@ -83,6 +82,12 @@
 		self.reset_default_field_value("set_warehouse", "items", "warehouse")
 		self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
 
+	def validate_material_request_type(self):
+		""" Validate fields in accordance with selected type """
+
+		if self.material_request_type != "Customer Provided":
+			self.customer = None
+
 	def set_title(self):
 		'''Set title as comma separated list of items'''
 		if not self.title:
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 5ab7929..d481689 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1540,6 +1540,7 @@
 		"conversion_factor": args.conversion_factor or 1.0,
 		"stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0),
 		"serial_no": args.serial_no,
+		"batch_no": args.batch_no,
 		"stock_uom": args.stock_uom or "_Test UOM",
 		"uom": uom,
 		"cost_center": args.cost_center or frappe.get_cached_value('Company',  pr.company,  'cost_center'),
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index c4b8131..5c9da3a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -425,6 +425,7 @@
 			'posting_time'		: frm.doc.posting_time,
 			'warehouse'			: cstr(item.s_warehouse) || cstr(item.t_warehouse),
 			'serial_no'			: item.serial_no,
+			'batch_no'          : item.batch_no,
 			'company'			: frm.doc.company,
 			'qty'				: item.s_warehouse ? -1*flt(item.transfer_qty) : flt(item.transfer_qty),
 			'voucher_type'		: frm.doc.doctype,
@@ -457,6 +458,7 @@
 						'warehouse': cstr(child.s_warehouse) || cstr(child.t_warehouse),
 						'transfer_qty': child.transfer_qty,
 						'serial_no': child.serial_no,
+						'batch_no': child.batch_no,
 						'qty': child.s_warehouse ? -1* child.transfer_qty : child.transfer_qty,
 						'posting_date': frm.doc.posting_date,
 						'posting_time': frm.doc.posting_time,
@@ -680,6 +682,7 @@
 				'warehouse'			: cstr(d.s_warehouse) || cstr(d.t_warehouse),
 				'transfer_qty'		: d.transfer_qty,
 				'serial_no'		: d.serial_no,
+				'batch_no'      : d.batch_no,
 				'bom_no'		: d.bom_no,
 				'expense_account'	: d.expense_account,
 				'cost_center'		: d.cost_center,
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 9ba007a..99cf4de 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -510,7 +510,7 @@
 				d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
 					self.doctype, self.name, d.allow_zero_valuation_rate,
 					currency=erpnext.get_company_currency(self.company), company=self.company,
-					raise_error_if_no_rate=raise_error_if_no_rate)
+					raise_error_if_no_rate=raise_error_if_no_rate, batch_no=d.batch_no)
 
 			d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
 			if d.is_process_loss:
@@ -541,6 +541,7 @@
 			"posting_time": self.posting_time,
 			"qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty),
 			"serial_no": item.serial_no,
+			"batch_no": item.batch_no,
 			"voucher_type": self.doctype,
 			"voucher_no": self.name,
 			"company": self.company,
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 306f2c3..6c6513b 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -1107,6 +1107,52 @@
 				posting_date='2021-09-02', # backdated consumption of 2nd batch
 				purpose='Material Issue')
 
+	def test_multi_batch_value_diff(self):
+		""" Test value difference on stock entry in case of multi-batch.
+			| Stock entry | batch | qty | rate | value diff on SE             |
+			| ---         | ---   | --- | ---  | ---                          |
+			| receipt     | A     | 1   | 10   | 30                           |
+			| receipt     | B     | 1   | 20   |                              |
+			| issue       | A     | -1  | 10   | -30 (to assert after submit) |
+			| issue       | B     | -1  | 20   |                              |
+		"""
+		from erpnext.stock.doctype.batch.test_batch import TestBatch
+
+		batch_nos = []
+
+		item_code = '_TestMultibatchFifo'
+		TestBatch.make_batch_item(item_code)
+		warehouse = '_Test Warehouse - _TC'
+		receipt = make_stock_entry(
+				item_code=item_code,
+				qty=1,
+				rate=10,
+				to_warehouse=warehouse,
+				purpose='Material Receipt',
+				do_not_save=True
+			)
+		receipt.append("items", frappe.copy_doc(receipt.items[0], ignore_no_copy=False).update({"basic_rate": 20}) )
+		receipt.save()
+		receipt.submit()
+		batch_nos.extend(row.batch_no for row in receipt.items)
+		self.assertEqual(receipt.value_difference, 30)
+
+		issue = make_stock_entry(
+				item_code=item_code,
+				qty=1,
+				from_warehouse=warehouse,
+				purpose='Material Issue',
+				do_not_save=True
+			)
+		issue.append("items", frappe.copy_doc(issue.items[0], ignore_no_copy=False))
+		for row, batch_no in zip(issue.items, batch_nos):
+			row.batch_no = batch_no
+		issue.save()
+		issue.submit()
+
+		issue.reload()  # reload because reposting current voucher updates rate
+		self.assertEqual(issue.value_difference, -30)
+
 def make_serialized_item(**args):
 	args = frappe._dict(args)
 	se = frappe.copy_doc(test_records[0])
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 a1030d5..0864ece 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
@@ -1,6 +1,10 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
 
+import json
+from operator import itemgetter
+from uuid import uuid4
+
 import frappe
 from frappe.core.page.permission_manager.permission_manager import reset
 from frappe.utils import add_days, today
@@ -349,6 +353,317 @@
 			frappe.set_user("Administrator")
 			user.remove_roles("Stock Manager")
 
+	def test_batchwise_item_valuation_moving_average(self):
+		item, warehouses, batches = setup_item_valuation_test(valuation_method="Moving Average")
+
+		# Incoming Entries for Stock Value check
+		pr_entry_list = [
+			(item, warehouses[0], batches[0], 1, 100),
+			(item, warehouses[0], batches[1], 1,  50),
+			(item, warehouses[0], batches[0], 1, 150),
+			(item, warehouses[0], batches[1], 1, 100),
+		]
+		prs = create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list)
+		sle_details = fetch_sle_details_for_doc_list(prs, ['stock_value'])
+		sv_list = [d['stock_value'] for d in sle_details]
+		expected_sv = [100, 150, 300, 400]
+		self.assertEqual(expected_sv, sv_list, "Incorrect 'Stock Value' values")
+
+		# Outgoing Entries for Stock Value Difference check
+		dn_entry_list = [
+			(item, warehouses[0], batches[1], 1, 200),
+			(item, warehouses[0], batches[0], 1, 200),
+			(item, warehouses[0], batches[1], 1, 200),
+			(item, warehouses[0], batches[0], 1, 200)
+		]
+		dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list)
+		sle_details = fetch_sle_details_for_doc_list(dns, ['stock_value_difference'])
+		svd_list = [-1 * d['stock_value_difference'] for d in sle_details]
+		expected_incoming_rates = expected_abs_svd = [75, 125, 75, 125]
+
+		self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values")
+		for dn, incoming_rate in zip(dns, expected_incoming_rates):
+			self.assertEqual(
+				dn.items[0].incoming_rate, incoming_rate,
+				"Incorrect 'Incoming Rate' values fetched for DN items"
+			)
+
+
+	def assertSLEs(self, doc, expected_sles):
+		""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
+		sles = frappe.get_all("Stock Ledger Entry", fields=["*"],
+				filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0},
+			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
+		}
+		def update_invariants(exp_sles):
+			for sle in exp_sles:
+				state["stock_value"] += sle["stock_value_difference"]
+				state["qty"] += sle["actual_qty"]
+				sle["stock_value"] = state["stock_value"]
+				sle["qty_after_transaction"] = state["qty"]
+
+		osr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=10, rate=100, batch_no=batches[1])
+		expected_sles = [
+			{"actual_qty": 10, "stock_value_difference": 1000},
+		]
+		update_invariants(expected_sles)
+		self.assertSLEs(osr1, expected_sles)
+
+		osr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=13, rate=200, batch_no=batches[0])
+		expected_sles = [
+			{"actual_qty": 13, "stock_value_difference": 200*13},
+		]
+		update_invariants(expected_sles)
+		self.assertSLEs(osr2, expected_sles)
+
+		sr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=5, rate=50, batch_no=batches[1])
+
+		expected_sles = [
+			{"actual_qty": -10, "stock_value_difference": -10 * 100},
+			{"actual_qty": 5, "stock_value_difference": 250}
+		]
+		update_invariants(expected_sles)
+		self.assertSLEs(sr1, expected_sles)
+
+		sr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=20, rate=75, batch_no=batches[0])
+		expected_sles = [
+			{"actual_qty": -13, "stock_value_difference": -13 * 200},
+			{"actual_qty": 20, "stock_value_difference": 20 * 75}
+		]
+		update_invariants(expected_sles)
+		self.assertSLEs(sr2, expected_sles)
+
+	def test_batch_wise_valuation_across_warehouse(self):
+		item_code, warehouses, batches = setup_item_valuation_test()
+		source = warehouses[0]
+		target = warehouses[1]
+
+		unrelated_batch = make_stock_entry(item_code=item_code, target=source, batch_no=batches[1],
+				qty=5, rate=10)
+		self.assertSLEs(unrelated_batch, [
+			{"actual_qty": 5, "stock_value_difference": 10 * 5},
+		])
+
+		reciept = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10)
+		self.assertSLEs(reciept, [
+			{"actual_qty": 5, "stock_value_difference": 10 * 5},
+		])
+
+		transfer = make_stock_entry(item_code=item_code, source=source, target=target, batch_no=batches[0], qty=5)
+		self.assertSLEs(transfer, [
+			{"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source},
+			{"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target}
+		])
+
+		backdated_receipt = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0],
+				qty=5, rate=20, posting_date=add_days(today(), -1))
+		self.assertSLEs(backdated_receipt, [
+			{"actual_qty": 5, "stock_value_difference": 20 * 5},
+		])
+
+		# check reposted average rate in *future* transfer
+		self.assertSLEs(transfer, [
+			{"actual_qty": -5, "stock_value_difference": -15 * 5, "warehouse": source, "stock_value": 15 * 5 + 10 * 5},
+			{"actual_qty": 5, "stock_value_difference": 15 * 5, "warehouse": target, "stock_value": 15 * 5}
+		])
+
+		transfer_unrelated = make_stock_entry(item_code=item_code, source=source,
+				target=target, batch_no=batches[1], qty=5)
+		self.assertSLEs(transfer_unrelated, [
+			{"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source, "stock_value": 15 * 5},
+			{"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target, "stock_value": 15 * 5 + 10 * 5}
+		])
+
+	def test_intermediate_average_batch_wise_valuation(self):
+		""" A batch has moving average up until posting time,
+		check if same is respected when backdated entry is inserted in middle"""
+		item_code, warehouses, batches = setup_item_valuation_test()
+		warehouse = warehouses[0]
+
+		batch = batches[0]
+
+		yesterday = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batch,
+				qty=1, rate=10, posting_date=add_days(today(), -1))
+		self.assertSLEs(yesterday, [
+			{"actual_qty": 1, "stock_value_difference": 10},
+		])
+
+		tomorrow = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+				qty=1, rate=30, posting_date=add_days(today(), 1))
+		self.assertSLEs(tomorrow, [
+			{"actual_qty": 1, "stock_value_difference": 30},
+		])
+
+		create_today = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+				qty=1, rate=20)
+		self.assertSLEs(create_today, [
+			{"actual_qty": 1, "stock_value_difference": 20},
+		])
+
+		consume_today = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0],
+				qty=1)
+		self.assertSLEs(consume_today, [
+			{"actual_qty": -1, "stock_value_difference": -15},
+		])
+
+		consume_tomorrow = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0],
+				qty=2, posting_date=add_days(today(), 2))
+		self.assertSLEs(consume_tomorrow, [
+			{"stock_value_difference": -(30 + 15), "stock_value": 0, "qty_after_transaction": 0},
+		])
+
+	def test_legacy_item_valuation_stock_entry(self):
+		columns = [
+				'stock_value_difference',
+				'stock_value',
+				'actual_qty',
+				'qty_after_transaction',
+				'stock_queue',
+		]
+		item, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0)
+
+		def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns):
+			for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)):
+				for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals):
+					if col == 'stock_queue':
+						sle_val = get_stock_value_from_q(sle_val)
+						ex_sle_val = get_stock_value_from_q(ex_sle_val)
+					self.assertEqual(
+						sle_val, ex_sle_val,
+						f"Incorrect {col} value on transaction #: {i} in {detail}"
+					)
+
+		# List used to defer assertions to prevent commits cause of error skipped rollback
+		details_list = []
+
+
+		# Test Material Receipt Entries
+		se_entry_list_mr = [
+			(item, None, warehouses[0], batches[0], 1,  50, "2021-01-21"),
+			(item, None, warehouses[0], batches[1], 1, 100, "2021-01-23"),
+		]
+		ses = create_stock_entry_entries_for_batchwise_item_valuation_test(
+			se_entry_list_mr, "Material Receipt"
+		)
+		sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0)
+		expected_sle_details = [
+			(50.0, 50.0, 1.0, 1.0, '[[1.0, 50.0]]'),
+			(100.0, 150.0, 1.0, 2.0, '[[1.0, 50.0], [1.0, 100.0]]'),
+		]
+		details_list.append((
+			sle_details, expected_sle_details,
+			"Material Receipt Entries", columns
+		))
+
+
+		# Test Material Issue Entries
+		se_entry_list_mi = [
+			(item, warehouses[0], None, batches[1], 1, None, "2021-01-29"),
+		]
+		ses = create_stock_entry_entries_for_batchwise_item_valuation_test(
+			se_entry_list_mi, "Material Issue"
+		)
+		sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0)
+		expected_sle_details = [
+			(-50.0, 100.0, -1.0, 1.0, '[[1, 100.0]]')
+		]
+		details_list.append((
+			sle_details, expected_sle_details,
+			"Material Issue Entries", columns
+		))
+
+
+		# Run assertions
+		for details in details_list:
+			check_sle_details_against_expected(*details)
+
+	def test_mixed_valuation_batches_fifo(self):
+		item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0)
+		warehouse = warehouses[0]
+
+		state = {
+			"qty": 0.0,
+			"stock_value": 0.0
+		}
+		def update_invariants(exp_sles):
+			for sle in exp_sles:
+				state["stock_value"] += sle["stock_value_difference"]
+				state["qty"] += sle["actual_qty"]
+				sle["stock_value"] = state["stock_value"]
+				sle["qty_after_transaction"] = state["qty"]
+			return exp_sles
+
+		old1 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+				qty=10, rate=10)
+		self.assertSLEs(old1, update_invariants([
+			{"actual_qty": 10, "stock_value_difference": 10*10, "stock_queue": [[10, 10]]},
+		]))
+		old2 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1],
+				qty=10, rate=20)
+		self.assertSLEs(old2, update_invariants([
+			{"actual_qty": 10, "stock_value_difference": 10*20, "stock_queue": [[10, 10], [10, 20]]},
+		]))
+		old3 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+				qty=5, rate=15)
+
+		self.assertSLEs(old3, update_invariants([
+			{"actual_qty": 5, "stock_value_difference": 5*15, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
+		]))
+
+		new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40)
+		batches.append(new1.items[0].batch_no)
+		# assert old queue remains
+		self.assertSLEs(new1, update_invariants([
+			{"actual_qty": 10, "stock_value_difference": 10*40, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
+		]))
+
+		new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42)
+		batches.append(new2.items[0].batch_no)
+		self.assertSLEs(new2, update_invariants([
+			{"actual_qty": 10, "stock_value_difference": 10*42, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
+		]))
+
+		# consume old batch as per FIFO
+		consume_old1 = make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0])
+		self.assertSLEs(consume_old1, update_invariants([
+			{"actual_qty": -15, "stock_value_difference": -10*10 - 5*20, "stock_queue": [[5, 20], [5, 15]]},
+		]))
+
+		# consume new batch as per batch
+		consume_new2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1])
+		self.assertSLEs(consume_new2, update_invariants([
+			{"actual_qty": -10, "stock_value_difference": -10*42, "stock_queue": [[5, 20], [5, 15]]},
+		]))
+
+		# finish all old batches
+		consume_old2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1])
+		self.assertSLEs(consume_old2, update_invariants([
+			{"actual_qty": -10, "stock_value_difference": -5*20 - 5*15, "stock_queue": []},
+		]))
+
+		# finish all new batches
+		consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2])
+		self.assertSLEs(consume_new1, update_invariants([
+			{"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
+		]))
 
 def create_repack_entry(**args):
 	args = frappe._dict(args)
@@ -412,3 +727,118 @@
 		make_item(d, properties=properties)
 
 	return items
+
+def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwise_valuation=1, batches_list=['X', 'Y']):
+	from erpnext.stock.doctype.batch.batch import make_batch
+	from erpnext.stock.doctype.item.test_item import make_item
+	from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+	if not suffix:
+		suffix = get_unique_suffix()
+
+	item = make_item(
+		f"IV - Test Item {valuation_method} {suffix}",
+		dict(valuation_method=valuation_method, has_batch_no=1, create_new_batch=1)
+	)
+	warehouses = [create_warehouse(f"IV - Test Warehouse {i}") for i in ['J', 'K']]
+	batches = [f"IV - Test Batch {i} {valuation_method} {suffix}" for i in batches_list]
+
+	for i, batch_id in enumerate(batches):
+		if not frappe.db.exists("Batch", batch_id):
+			ubw = use_batchwise_valuation
+			if isinstance(use_batchwise_valuation, (list, tuple)):
+				ubw = use_batchwise_valuation[i]
+			batch = frappe.get_doc(frappe._dict(
+					doctype="Batch",
+					batch_id=batch_id,
+					item=item.item_code,
+					use_batchwise_valuation=ubw
+				)
+			).insert()
+			batch.use_batchwise_valuation = ubw
+			batch.db_update()
+
+	return item.item_code, warehouses, batches
+
+def create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list):
+	from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+	prs = []
+
+	for item, warehouse, batch_no, qty, rate in pr_entry_list:
+		pr = make_purchase_receipt(item=item, warehouse=warehouse, qty=qty, rate=rate, batch_no=batch_no)
+		prs.append(pr)
+
+	return prs
+
+def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list):
+	from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+	from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+	dns = []
+	for item, warehouse, batch_no, qty, rate in dn_entry_list:
+		so = make_sales_order(
+			rate=rate,
+			qty=qty,
+			item=item,
+			warehouse=warehouse,
+			against_blanket_order=0
+		)
+
+		dn = make_delivery_note(so.name)
+		dn.items[0].batch_no = batch_no
+		dn.insert()
+		dn.submit()
+		dns.append(dn)
+	return dns
+
+def fetch_sle_details_for_doc_list(doc_list, columns, as_dict=1):
+	return frappe.db.sql(f"""
+		SELECT { ', '.join(columns)}
+		FROM `tabStock Ledger Entry`
+		WHERE
+			voucher_no IN %(voucher_nos)s
+			and docstatus = 1
+		ORDER BY timestamp(posting_date, posting_time) ASC, CREATION ASC
+	""", dict(
+		voucher_nos=[doc.name for doc in doc_list]
+	), as_dict=as_dict)
+
+def get_stock_value_from_q(q):
+	return sum(r*q for r,q in json.loads(q))
+
+def create_stock_entry_entries_for_batchwise_item_valuation_test(se_entry_list, purpose):
+	ses = []
+	for item, source, target, batch, qty, rate, posting_date in se_entry_list:
+		args = dict(
+			item_code=item,
+			qty=qty,
+			company="_Test Company",
+			batch_no=batch,
+			posting_date=posting_date,
+			purpose=purpose
+		)
+
+		if purpose == "Material Receipt":
+			args.update(
+				dict(to_warehouse=target, rate=rate)
+			)
+
+		elif purpose == "Material Issue":
+			args.update(
+				dict(from_warehouse=source)
+			)
+
+		elif purpose == "Material Transfer":
+			args.update(
+				dict(from_warehouse=source, to_warehouse=target)
+			)
+
+		else:
+			raise ValueError(f"Invalid purpose: {purpose}")
+		ses.append(make_stock_entry(**args))
+
+	return ses
+
+def get_unique_suffix():
+	# Used to isolate valuation sensitive
+	# tests to prevent future tests from failing.
+	return str(uuid4())[:8].upper()
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 86af0a0..2ffe127 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -200,7 +200,6 @@
 
 	def test_stock_reco_for_batch_item(self):
 		to_delete_records = []
-		to_delete_serial_nos = []
 
 		# Add new serial nos
 		item_code = "Stock-Reco-batch-Item-1"
@@ -208,20 +207,22 @@
 
 		sr = create_stock_reconciliation(item_code=item_code,
 			warehouse = warehouse, qty=5, rate=200, do_not_submit=1)
-		sr.save(ignore_permissions=True)
+		sr.save()
 		sr.submit()
 
-		self.assertTrue(sr.items[0].batch_no)
+		batch_no = sr.items[0].batch_no
+		self.assertTrue(batch_no)
 		to_delete_records.append(sr.name)
 
 		sr1 = create_stock_reconciliation(item_code=item_code,
-			warehouse = warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no)
+			warehouse = warehouse, qty=6, rate=300, batch_no=batch_no)
 
 		args = {
 			"item_code": item_code,
 			"warehouse": warehouse,
 			"posting_date": nowdate(),
 			"posting_time": nowtime(),
+			"batch_no": batch_no,
 		}
 
 		valuation_rate = get_incoming_rate(args)
@@ -230,7 +231,7 @@
 
 
 		sr2 = create_stock_reconciliation(item_code=item_code,
-			warehouse = warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no)
+			warehouse = warehouse, qty=0, rate=0, batch_no=batch_no)
 
 		stock_value = get_stock_value_on(warehouse, nowdate(), item_code)
 		self.assertEqual(stock_value, 0)
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index a89a403..97a740e 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -12,6 +12,7 @@
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 
 Filters = frappe._dict
+precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
 
 def execute(filters: Filters = None) -> Tuple:
 	to_date = filters["to_date"]
@@ -48,10 +49,13 @@
 		if filters.get("show_warehouse_wise_stock"):
 			row.append(details.warehouse)
 
-		row.extend([item_dict.get("total_qty"), average_age,
+		row.extend([
+			flt(item_dict.get("total_qty"), precision),
+			average_age,
 			range1, range2, range3, above_range3,
 			earliest_age, latest_age,
-			details.stock_uom])
+			details.stock_uom
+		])
 
 		data.append(row)
 
@@ -79,13 +83,13 @@
 		qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0
 
 		if age <= filters.range1:
-			range1 += qty
+			range1 = flt(range1 + qty, precision)
 		elif age <= filters.range2:
-			range2 += qty
+			range2 = flt(range2 + qty, precision)
 		elif age <= filters.range3:
-			range3 += qty
+			range3 = flt(range3 + qty, precision)
 		else:
-			above_range3 += qty
+			above_range3 = flt(above_range3 + qty, precision)
 
 	return range1, range2, range3, above_range3
 
@@ -286,14 +290,16 @@
 	def __compute_incoming_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List):
 		"Update FIFO Queue on inward stock."
 
-		if self.transferred_item_details.get(transfer_key):
+		transfer_data = self.transferred_item_details.get(transfer_key)
+		if transfer_data:
 			# inward/outward from same voucher, item & warehouse
-			slot = self.transferred_item_details[transfer_key].pop(0)
-			fifo_queue.append(slot)
+			# eg: Repack with same item, Stock reco for batch item
+			# consume transfer data and add stock to fifo queue
+			self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row)
 		else:
 			if not serial_nos:
-				if fifo_queue and flt(fifo_queue[0][0]) < 0:
-					# neutralize negative stock by adding positive stock
+				if fifo_queue and flt(fifo_queue[0][0]) <= 0:
+					# neutralize 0/negative stock by adding positive stock
 					fifo_queue[0][0] += flt(row.actual_qty)
 					fifo_queue[0][1] = row.posting_date
 				else:
@@ -324,7 +330,7 @@
 			elif not fifo_queue:
 				# negative stock, no balance but qty yet to consume
 				fifo_queue.append([-(qty_to_pop), row.posting_date])
-				self.transferred_item_details[transfer_key].append([row.actual_qty, row.posting_date])
+				self.transferred_item_details[transfer_key].append([qty_to_pop, row.posting_date])
 				qty_to_pop = 0
 			else:
 				# qty to pop < slot qty, ample balance
@@ -333,6 +339,33 @@
 				self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]])
 				qty_to_pop = 0
 
+	def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict):
+		"Add previously removed stock back to FIFO Queue."
+		transfer_qty_to_pop = flt(row.actual_qty)
+
+		def add_to_fifo_queue(slot):
+			if fifo_queue and flt(fifo_queue[0][0]) <= 0:
+				# neutralize 0/negative stock by adding positive stock
+				fifo_queue[0][0] += flt(slot[0])
+				fifo_queue[0][1] = slot[1]
+			else:
+				fifo_queue.append(slot)
+
+		while transfer_qty_to_pop:
+			if transfer_data and 0 < transfer_data[0][0] <= transfer_qty_to_pop:
+				# bucket qty is not enough, consume whole
+				transfer_qty_to_pop -= transfer_data[0][0]
+				add_to_fifo_queue(transfer_data.pop(0))
+			elif not transfer_data:
+				# transfer bucket is empty, extra incoming qty
+				add_to_fifo_queue([transfer_qty_to_pop, row.posting_date])
+				transfer_qty_to_pop = 0
+			else:
+				# ample bucket qty to consume
+				transfer_data[0][0] -= transfer_qty_to_pop
+				add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1]])
+				transfer_qty_to_pop = 0
+
 	def __update_balances(self, row: Dict, key: Union[Tuple, str]):
 		self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction
 
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
index 9e9bed4..3d759dd 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
+++ b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
@@ -71,4 +71,39 @@
 2nd  | -60 | [[-10, 1-12-2021]]
 3rd  | +5  | [[-5, 3-12-2021]]
 4th  | +10 | [[5, 4-12-2021]]
-4th  | +20 | [[5, 4-12-2021], [20, 4-12-2021]]
\ No newline at end of file
+4th  | +20 | [[5, 4-12-2021], [20, 4-12-2021]]
+
+### Concept of Transfer Qty Bucket
+In the case of **Repack**, Quantity that comes in, isn't really incoming. It is just new stock repurposed from old stock, due to incoming-outgoing of the same warehouse.
+
+Here, stock is consumed from the FIFO Queue. It is then re-added back to the queue.
+While adding stock back to the queue we need to know how much to add.
+For this we need to keep track of how much was previously consumed.
+Hence we use **Transfer Qty Bucket**.
+
+While re-adding stock, we try to add buckets that were consumed earlier (date intact), to maintain correctness.
+
+#### Case 1: Same Item-Warehouse in Repack
+Eg:
+-------------------------------------------------------------------------------------
+Date | Qty   | Voucher |             FIFO Queue           	   | Transfer Qty Buckets
+-------------------------------------------------------------------------------------
+1st  | +500  |  PR     | [[500, 1-12-2021]]   				   |
+2nd  | -50   |  Repack | [[450, 1-12-2021]]   				   | [[50, 1-12-2021]]
+2nd  | +50   |  Repack | [[450, 1-12-2021], [50, 1-12-2021]]   | []
+
+- The balance at the end is restored back to 500
+- However, the initial 500 qty bucket is now split into 450 and 50, with the same date
+- The net effect is the same as that before the Repack
+
+#### Case 2: Same Item-Warehouse in Repack with Split Consumption rows
+Eg:
+-------------------------------------------------------------------------------------
+Date | Qty   | Voucher |             FIFO Queue           	   | Transfer Qty Buckets
+-------------------------------------------------------------------------------------
+1st  | +500  |  PR     | [[500, 1-12-2021]]   				   |
+2nd  | -50   |  Repack | [[450, 1-12-2021]]   				   | [[50, 1-12-2021]]
+2nd  | -50   |  Repack | [[400, 1-12-2021]]   				   | [[50, 1-12-2021],
+-	 |		 |		   |									   |[50, 1-12-2021]]
+2nd  | +100  |  Repack | [[400, 1-12-2021], [50, 1-12-2021],   | []
+-	 |		 |		   | [50, 1-12-2021]]					   |
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
index 66d2f6b..3fc357e 100644
--- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -3,7 +3,7 @@
 
 import frappe
 
-from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots
+from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data
 from erpnext.tests.utils import ERPNextTestCase
 
 
@@ -11,7 +11,8 @@
 	def setUp(self) -> None:
 		self.filters = frappe._dict(
 			company="_Test Company",
-			to_date="2021-12-10"
+			to_date="2021-12-10",
+			range1=30, range2=60, range3=90
 		)
 
 	def test_normal_inward_outward_queue(self):
@@ -236,6 +237,371 @@
 		item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots]
 		self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"])
 
+	def test_repack_entry_same_item_split_rows(self):
+		"""
+		Split consumption rows and have single repacked item row (same warehouse).
+		Ledger:
+		Item	| Qty | Voucher
+		------------------------
+		Item 1  | 500 | 001
+		Item 1  | -50 | 002 (repack)
+		Item 1  | -50 | 002 (repack)
+		Item 1  | 100 | 002 (repack)
+
+		Case most likely for batch items. Test time bucket computation.
+		"""
+		sle = [
+			frappe._dict( # stock up item
+				name="Flask Item",
+				actual_qty=500, qty_after_transaction=500,
+				warehouse="WH 1",
+				posting_date="2021-12-03", voucher_type="Stock Entry",
+				voucher_no="001",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=(-50), qty_after_transaction=450,
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=(-50), qty_after_transaction=400,
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=100, qty_after_transaction=500,
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+		]
+		slots = FIFOSlots(self.filters, sle).generate()
+		item_result = slots["Flask Item"]
+		queue = item_result["fifo_queue"]
+
+		self.assertEqual(item_result["total_qty"], 500.0)
+		self.assertEqual(queue[0][0], 400.0)
+		self.assertEqual(queue[1][0], 50.0)
+		self.assertEqual(queue[2][0], 50.0)
+		# check if time buckets add up to balance qty
+		self.assertEqual(sum([i[0] for i in queue]), 500.0)
+
+	def test_repack_entry_same_item_overconsume(self):
+		"""
+		Over consume item and have less repacked item qty (same warehouse).
+		Ledger:
+		Item	| Qty  | Voucher
+		------------------------
+		Item 1  | 500  | 001
+		Item 1  | -100 | 002 (repack)
+		Item 1  | 50   | 002 (repack)
+
+		Case most likely for batch items. Test time bucket computation.
+		"""
+		sle = [
+			frappe._dict( # stock up item
+				name="Flask Item",
+				actual_qty=500, qty_after_transaction=500,
+				warehouse="WH 1",
+				posting_date="2021-12-03", voucher_type="Stock Entry",
+				voucher_no="001",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=(-100), qty_after_transaction=400,
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=50, qty_after_transaction=450,
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+		]
+		slots = FIFOSlots(self.filters, sle).generate()
+		item_result = slots["Flask Item"]
+		queue = item_result["fifo_queue"]
+
+		self.assertEqual(item_result["total_qty"], 450.0)
+		self.assertEqual(queue[0][0], 400.0)
+		self.assertEqual(queue[1][0], 50.0)
+		# check if time buckets add up to balance qty
+		self.assertEqual(sum([i[0] for i in queue]), 450.0)
+
+	def test_repack_entry_same_item_overconsume_with_split_rows(self):
+		"""
+		Over consume item and have less repacked item qty (same warehouse).
+		Ledger:
+		Item	| Qty  | Voucher
+		------------------------
+		Item 1  | 20   | 001
+		Item 1  | -50  | 002 (repack)
+		Item 1  | -50  | 002 (repack)
+		Item 1  | 50   | 002 (repack)
+		"""
+		sle = [
+			frappe._dict( # stock up item
+				name="Flask Item",
+				actual_qty=20, qty_after_transaction=20,
+				warehouse="WH 1",
+				posting_date="2021-12-03", voucher_type="Stock Entry",
+				voucher_no="001",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=(-50), qty_after_transaction=(-30),
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=(-50), qty_after_transaction=(-80),
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=50, qty_after_transaction=(-30),
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+		]
+		fifo_slots = FIFOSlots(self.filters, sle)
+		slots = fifo_slots.generate()
+		item_result = slots["Flask Item"]
+		queue = item_result["fifo_queue"]
+
+		self.assertEqual(item_result["total_qty"], -30.0)
+		self.assertEqual(queue[0][0], -30.0)
+
+		# check transfer bucket
+		transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')]
+		self.assertEqual(transfer_bucket[0][0], 50)
+
+	def test_repack_entry_same_item_overproduce(self):
+		"""
+		Under consume item and have more repacked item qty (same warehouse).
+		Ledger:
+		Item	| Qty  | Voucher
+		------------------------
+		Item 1  | 500  | 001
+		Item 1  | -50  | 002 (repack)
+		Item 1  | 100  | 002 (repack)
+
+		Case most likely for batch items. Test time bucket computation.
+		"""
+		sle = [
+			frappe._dict( # stock up item
+				name="Flask Item",
+				actual_qty=500, qty_after_transaction=500,
+				warehouse="WH 1",
+				posting_date="2021-12-03", voucher_type="Stock Entry",
+				voucher_no="001",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=(-50), qty_after_transaction=450,
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=100, qty_after_transaction=550,
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+		]
+		slots = FIFOSlots(self.filters, sle).generate()
+		item_result = slots["Flask Item"]
+		queue = item_result["fifo_queue"]
+
+		self.assertEqual(item_result["total_qty"], 550.0)
+		self.assertEqual(queue[0][0], 450.0)
+		self.assertEqual(queue[1][0], 50.0)
+		self.assertEqual(queue[2][0], 50.0)
+		# check if time buckets add up to balance qty
+		self.assertEqual(sum([i[0] for i in queue]), 550.0)
+
+	def test_repack_entry_same_item_overproduce_with_split_rows(self):
+		"""
+		Over consume item and have less repacked item qty (same warehouse).
+		Ledger:
+		Item	| Qty  | Voucher
+		------------------------
+		Item 1  | 20   | 001
+		Item 1  | -50  | 002 (repack)
+		Item 1  | 50  | 002 (repack)
+		Item 1  | 50   | 002 (repack)
+		"""
+		sle = [
+			frappe._dict( # stock up item
+				name="Flask Item",
+				actual_qty=20, qty_after_transaction=20,
+				warehouse="WH 1",
+				posting_date="2021-12-03", voucher_type="Stock Entry",
+				voucher_no="001",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=(-50), qty_after_transaction=(-30),
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=50, qty_after_transaction=20,
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict(
+				name="Flask Item",
+				actual_qty=50, qty_after_transaction=70,
+				warehouse="WH 1",
+				posting_date="2021-12-04", voucher_type="Stock Entry",
+				voucher_no="002",
+				has_serial_no=False, serial_no=None
+			),
+		]
+		fifo_slots = FIFOSlots(self.filters, sle)
+		slots = fifo_slots.generate()
+		item_result = slots["Flask Item"]
+		queue = item_result["fifo_queue"]
+
+		self.assertEqual(item_result["total_qty"], 70.0)
+		self.assertEqual(queue[0][0], 20.0)
+		self.assertEqual(queue[1][0], 50.0)
+
+		# check transfer bucket
+		transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')]
+		self.assertFalse(transfer_bucket)
+
+	def test_negative_stock_same_voucher(self):
+		"""
+		Test negative stock scenario in transfer bucket via repack entry (same wh).
+		Ledger:
+		Item	| Qty  | Voucher
+		------------------------
+		Item 1  | -50  | 001
+		Item 1  | -50  | 001
+		Item 1  | 30   | 001
+		Item 1  | 80   | 001
+		"""
+		sle = [
+			frappe._dict( # stock up item
+				name="Flask Item",
+				actual_qty=(-50), qty_after_transaction=(-50),
+				warehouse="WH 1",
+				posting_date="2021-12-01", voucher_type="Stock Entry",
+				voucher_no="001",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict( # stock up item
+				name="Flask Item",
+				actual_qty=(-50), qty_after_transaction=(-100),
+				warehouse="WH 1",
+				posting_date="2021-12-01", voucher_type="Stock Entry",
+				voucher_no="001",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict( # stock up item
+				name="Flask Item",
+				actual_qty=30, qty_after_transaction=(-70),
+				warehouse="WH 1",
+				posting_date="2021-12-01", voucher_type="Stock Entry",
+				voucher_no="001",
+				has_serial_no=False, serial_no=None
+			),
+		]
+		fifo_slots = FIFOSlots(self.filters, sle)
+		slots = fifo_slots.generate()
+		item_result = slots["Flask Item"]
+
+		# check transfer bucket
+		transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')]
+		self.assertEqual(transfer_bucket[0][0], 20)
+		self.assertEqual(transfer_bucket[1][0], 50)
+		self.assertEqual(item_result["fifo_queue"][0][0], -70.0)
+
+		sle.append(frappe._dict(
+			name="Flask Item",
+			actual_qty=80, qty_after_transaction=10,
+			warehouse="WH 1",
+			posting_date="2021-12-01", voucher_type="Stock Entry",
+			voucher_no="001",
+			has_serial_no=False, serial_no=None
+		))
+
+		fifo_slots = FIFOSlots(self.filters, sle)
+		slots = fifo_slots.generate()
+		item_result = slots["Flask Item"]
+
+		transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')]
+		self.assertFalse(transfer_bucket)
+		self.assertEqual(item_result["fifo_queue"][0][0], 10.0)
+
+	def test_precision(self):
+		"Test if final balance qty is rounded off correctly."
+		sle = [
+			frappe._dict( # stock up item
+				name="Flask Item",
+				actual_qty=0.3, qty_after_transaction=0.3,
+				warehouse="WH 1",
+				posting_date="2021-12-01", voucher_type="Stock Entry",
+				voucher_no="001",
+				has_serial_no=False, serial_no=None
+			),
+			frappe._dict( # stock up item
+				name="Flask Item",
+				actual_qty=0.6, qty_after_transaction=0.9,
+				warehouse="WH 1",
+				posting_date="2021-12-01", voucher_type="Stock Entry",
+				voucher_no="001",
+				has_serial_no=False, serial_no=None
+			),
+		]
+
+		slots = FIFOSlots(self.filters, sle).generate()
+		report_data = format_report_data(self.filters, slots, self.filters["to_date"])
+		row = report_data[0] # first row in report
+		bal_qty = row[5]
+		range_qty_sum = sum([i for i in row[7:11]]) # get sum of range balance
+
+		# check if value of Available Qty column matches with range bucket post format
+		self.assertEqual(bal_qty, 0.9)
+		self.assertEqual(bal_qty, range_qty_sum)
+
 def generate_item_and_item_wh_wise_slots(filters, sle):
 	"Return results with and without 'show_warehouse_wise_stock'"
 	item_wise_slots = FIFOSlots(filters, sle).generate()
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
index cb35bf7..7826d34 100644
--- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -60,6 +60,9 @@
 			fifo_qty += qty
 			fifo_value += qty * rate
 
+		if sle.actual_qty < 0:
+			sle.consumption_rate = sle.stock_value_difference  / sle.actual_qty
+
 		balance_qty += sle.actual_qty
 		balance_stock_value += sle.stock_value_difference
 		if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
@@ -90,6 +93,9 @@
 			sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
 			sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
 
+		if sle.batch_no:
+			sle.use_batchwise_valuation = frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
+
 	return sles
 
 
@@ -135,6 +141,11 @@
 			"options": "Batch",
 		},
 		{
+			"fieldname": "use_batchwise_valuation",
+			"fieldtype": "Check",
+			"label": "Batchwise Valuation",
+		},
+		{
 			"fieldname": "actual_qty",
 			"fieldtype": "Float",
 			"label": "Qty Change",
@@ -145,9 +156,9 @@
 			"label": "Incoming Rate",
 		},
 		{
-			"fieldname": "outgoing_rate",
+			"fieldname": "consumption_rate",
 			"fieldtype": "Float",
-			"label": "Outgoing Rate",
+			"label": "Consumption Rate",
 		},
 		{
 			"fieldname": "qty_after_transaction",
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index 525af40..76c2079 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -73,10 +73,11 @@
 	def test_execute_all_stock_reports(self):
 		"""Test that all script report in stock modules are executable with supported filters"""
 		for report, filter in REPORT_FILTER_TEST_CASES:
-			execute_script_report(
-				report_name=report,
-				module="Stock",
-				filters=filter,
-				default_filters=DEFAULT_FILTERS,
-				optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
-			)
+			with self.subTest(report=report):
+				execute_script_report(
+					report_name=report,
+					module="Stock",
+					filters=filter,
+					default_filters=DEFAULT_FILTERS,
+					optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+				)
diff --git a/erpnext/stock/spec/README.md b/erpnext/stock/spec/README.md
new file mode 100644
index 0000000..f5a3501
--- /dev/null
+++ b/erpnext/stock/spec/README.md
@@ -0,0 +1,103 @@
+# Implementation notes for Stock Ledger
+
+
+## Important files
+
+- `stock/stock_ledger.py`
+- `controllers/stock_controller.py`
+- `stock/valuation.py`
+
+## What is in an Stock Ledger Entry (SLE)?
+
+Stock Ledger Entry is a single row in the Stock Ledger. It signifies some
+modification of stock for a particular Item in the specified warehouse.
+
+- `item_code`: item for which ledger entry is made
+- `warehouse`: warehouse where inventory is affected
+- `actual_qty`: change in qty
+- `qty_after_transaction`: quantity available after the transaction is processed
+- `incoming_rate`: rate at which inventory was received.
+- `is_cancelled`: if 1 then stock ledger entry is cancelled and should not be used
+for any business logic except for the code that handles cancellation.
+- `posting_date` & `posting_time`: Specify the temporal ordering of stock ledger
+  entries. Ties are broken by `creation` timestamp.
+- `voucher_type`: Many transaction can create SLE, e.g. Stock Entry, Purchase
+  Invoice
+- `voucher_no`: `name` of the transaction that created SLE
+- `voucher_detail_no`: `name` of the child table row from parent transaction
+  that created the SLE.
+- `dependant_sle_voucher_detail_no`: cross-warehouse transfers need this
+  reference in order to update dependent warehouse rates in case of change in
+  rate.
+- `recalculate_rate`: if this is checked in/out rates are recomputed on
+  transactions.
+- `valuation_rate`: current average valuation rate.
+- `stock_value`: current total stock value
+- `stock_value_difference`: stock value difference made between last and current
+  entry. This value is booked in accounting ledger.
+- `stock_queue`: if FIFO/LIFO is used this represents queue/stack maintained for
+  computing incoming rate for inventory getting consumed.
+- `batch_no`: batch no for which stock entry is made; each stock entry can only
+  affect one batch number.
+- `serial_no`: newline separated list of serial numbers that were added (if
+  actual_qty > 0) or else removed. Currently multiple serial nos can have single
+  SLE but this will likely change in future.
+
+
+## Implementation of Stock Ledger
+
+Stock Ledger Entry affects stock of combinations of (item_code, warehouse) and
+optionally batch no if specified. For simplicity, lets avoid batch no. for now.
+
+
+Stock Ledger Entry table stores stock ledger for all combinations of item_code
+and warehouse. So whenever any operations are to be performed on said
+item-warehouse combination stock ledger is filtered and sorted by posting
+datetime. A typical query that will give you individual ledger looks like this:
+
+```sql
+select *
+from `tabStock Ledger Entry` as sle
+where
+    is_cancelled = 0  --- cancelled entries don't affect ledger
+    and item_code = 'item_code' and warehouse = 'warehouse_name'
+order by timestamp(posting_date, posting_time), creation
+```
+
+New entry is just an update to the last entry which is found by looking at last
+row in the filter ledger.
+
+
+### Serial nos
+
+Serial numbers do not follow any valuation method configuration and they are
+consumed at rate they were produced unless they are grouped in which case they
+are consumed at weighted average rate.
+
+
+### Batch Nos
+
+Batches are currently NOT consumed as per batch wise valuation rate, instead
+global FIFO queue for the item is used for valuation rate.
+
+
+## Creation process of SLEs
+
+- SLE creation is usually triggered by Stock Transactions using a method
+  conventionally named `update_stock_ledger()` This might not be defined for
+  stock transaction and could be specified somewhere in inheritance hierarchy of
+  controllers.
+- This method produces SLE objects which are processed by `make_sl_entries` in
+  `stock_ledger.py` which commits the SLE to database.
+- `update_entries_after` class is used to process ONLY the inserted SLE's queue
+  and valuation.
+- The change in qty is propagated to future entries immediately. Valuation and
+  queue for future entries is processed in background using repost item
+  valuation.
+
+
+## Accounting impact
+
+- Accounting impact for stock transaction is handled by `get_gl_entries()`
+  method on controllers. Each transaction has different business logic for
+  booking the accounting impact.
diff --git a/erpnext/stock/spec/reposting.md b/erpnext/stock/spec/reposting.md
new file mode 100644
index 0000000..b0d59fe
--- /dev/null
+++ b/erpnext/stock/spec/reposting.md
@@ -0,0 +1,38 @@
+# Stock Reposting
+
+Stock "reposting" is process of re-processing Stock Ledger Entry and GL Entries
+in event of backdated stock transaction.
+
+*Backdated stock transaction*: Any stock transaction for which some
+item-warehouse combination has a future transactions.
+
+## Why is this required?
+Stock Ledger is stateful, it maintains queue, qty at any
+point in time. So if you do a backdated transaction all future values change,
+queues need to be re-evaluated etc. Watch Nabin and Rohit's conference
+presentation for explanation: https://www.youtube.com/watch?v=mw3WAnekGIM
+
+## How is this implemented?
+Whenever backdated transaction is detected, instead of
+fully processing it while submitting, the processing is queued using "Repost
+Item Valuation" doctype. Every hour a scheduled job runs and processes this
+queue (for up to maximum of 25 minutes)
+
+
+## Queue implementation
+- "Repost item valuation" (RIV) is automatically submitted from backdated transactions. (check stock_controller.py)
+- Draft and cancelled RIV are ignored.
+- Keep filter of "submitted" documents when doing anything with RIVs.
+- The default status is "Queued".
+- When background job runs, it picks the oldest pending reposts and changes the status to "In Progress" and when it finishes it
+changes to "Completed"
+- There are two more status: "Failed" when reposting failed and "Skipped" when reposting is deemed not necessary so it's skipped.
+- technical detail: Entry point for whole process is "repost_entries" function in repost_item_valuation.py
+
+
+## How to identify broken stock data:
+There are 4 major reports for checking broken stock data:
+- Incorrect balance qty after the transaction - to check if the running total of qty isn't correct.
+- Incorrect stock value report - to check incorrect value books in accounts for stock transactions
+- Incorrect serial no valuation -specific to serial nos
+- Stock ledger invariant check - combined report for checking qty, running total, queue, balance value etc
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 00ca81f..1b90086 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -8,7 +8,9 @@
 import frappe
 from frappe import _
 from frappe.model.meta import get_field_precision
+from frappe.query_builder.functions import Sum
 from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
+from pypika import CustomFunction
 
 import erpnext
 from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
@@ -17,14 +19,13 @@
 	get_or_make_bin,
 	get_valuation_method,
 )
-from erpnext.stock.valuation import FIFOValuation, LIFOValuation
+from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
 
 
 class NegativeStockError(frappe.ValidationError): pass
 class SerialNoExistsInFutureTransaction(frappe.ValidationError):
 	pass
 
-_exceptions = frappe.local('stockledger_exceptions')
 
 def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
 	from erpnext.controllers.stock_controller import future_sle_exists
@@ -447,6 +448,8 @@
 				self.wh_data.qty_after_transaction = sle.qty_after_transaction
 
 			self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
+		elif sle.batch_no and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True):
+			self.update_batched_values(sle)
 		else:
 			if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
 				# assert
@@ -462,10 +465,11 @@
 					self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
 				else:
 					self.update_queue_values(sle)
-					self.wh_data.qty_after_transaction += flt(sle.actual_qty)
 
 		# rounding as per precision
 		self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
+		if not self.wh_data.qty_after_transaction:
+			self.wh_data.stock_value = 0.0
 		stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
 		self.wh_data.prev_stock_value = self.wh_data.stock_value
 
@@ -481,6 +485,7 @@
 		if not self.args.get("sle_id"):
 			self.update_outgoing_rate_on_transaction(sle)
 
+
 	def validate_negative_stock(self, sle):
 		"""
 			validate negative stock for entries current datetime onwards
@@ -629,9 +634,7 @@
 		if not self.wh_data.valuation_rate and sle.voucher_detail_no:
 			allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
 			if not allow_zero_rate:
-				self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
-					sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
-					currency=erpnext.get_company_currency(sle.company), company=sle.company)
+				self.wh_data.valuation_rate = self.get_fallback_rate(sle)
 
 	def get_incoming_value_for_serial_nos(self, sle, serial_nos):
 		# get rate from serial nos within same company
@@ -697,46 +700,70 @@
 			if not self.wh_data.valuation_rate and sle.voucher_detail_no:
 				allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
 				if not allow_zero_valuation_rate:
-					self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
-						sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
-						currency=erpnext.get_company_currency(sle.company), company=sle.company)
+					self.wh_data.valuation_rate = self.get_fallback_rate(sle)
 
 	def update_queue_values(self, sle):
 		incoming_rate = flt(sle.incoming_rate)
 		actual_qty = flt(sle.actual_qty)
 		outgoing_rate = flt(sle.outgoing_rate)
 
+		self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty)
+
 		if self.valuation_method == "LIFO":
 			stock_queue = LIFOValuation(self.wh_data.stock_queue)
 		else:
 			stock_queue = FIFOValuation(self.wh_data.stock_queue)
 
+		_prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
+
 		if actual_qty > 0:
 			stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
 		else:
 			def rate_generator() -> float:
 				allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
 				if not allow_zero_valuation_rate:
-					return get_valuation_rate(sle.item_code, sle.warehouse,
-						sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
-						currency=erpnext.get_company_currency(sle.company), company=sle.company)
+					return self.get_fallback_rate(sle)
 				else:
 					return 0.0
 
 			stock_queue.remove_stock(qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator)
 
-		stock_qty, stock_value = stock_queue.get_total_stock_and_value()
+		_qty, stock_value = stock_queue.get_total_stock_and_value()
+
+		stock_value_difference = stock_value - prev_stock_value
 
 		self.wh_data.stock_queue = stock_queue.state
-		self.wh_data.stock_value = stock_value
-		if stock_qty:
-			self.wh_data.valuation_rate = stock_value / stock_qty
-
+		self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
 
 		if not self.wh_data.stock_queue:
 			self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate])
 
+		if self.wh_data.qty_after_transaction:
+			self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
 
+	def update_batched_values(self, sle):
+		incoming_rate = flt(sle.incoming_rate)
+		actual_qty = flt(sle.actual_qty)
+
+		self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty)
+
+		if actual_qty > 0:
+			stock_value_difference = incoming_rate * actual_qty
+		else:
+			outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code,
+					warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date,
+					posting_time=sle.posting_time, creation=sle.creation)
+			if outgoing_rate is None:
+				# This can *only* happen if qty available for the batch is zero.
+				# in such case fall back various other rates.
+				# future entries will correct the overall accounting as each
+				# batch individually uses moving average rates.
+				outgoing_rate = self.get_fallback_rate(sle)
+			stock_value_difference = outgoing_rate * actual_qty
+
+		self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
+		if self.wh_data.qty_after_transaction:
+			self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
 
 	def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
 		ref_item_dt = ""
@@ -751,6 +778,13 @@
 		else:
 			return 0
 
+	def get_fallback_rate(self, sle) -> float:
+		"""When exact incoming rate isn't available use any of other "average" rates as fallback.
+			This should only get used for negative stock."""
+		return get_valuation_rate(sle.item_code, sle.warehouse,
+			sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
+			currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
+
 	def get_sle_before_datetime(self, args):
 		"""get previous stock ledger entry before current time-bucket"""
 		sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
@@ -897,22 +931,72 @@
 		['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'],
 		as_dict=1)
 
+def get_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None):
+
+	Timestamp = CustomFunction('timestamp', ['date', 'time'])
+
+	sle = frappe.qb.DocType("Stock Ledger Entry")
+
+	timestamp_condition = (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(posting_date, posting_time))
+	if creation:
+		timestamp_condition |= (
+				(Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time))
+				& (sle.creation < creation)
+			)
+
+	batch_details = (
+		frappe.qb
+			.from_(sle)
+			.select(
+				Sum(sle.stock_value_difference).as_("batch_value"),
+				Sum(sle.actual_qty).as_("batch_qty")
+			)
+			.where(
+				(sle.item_code == item_code)
+				& (sle.warehouse == warehouse)
+				& (sle.batch_no == batch_no)
+				& (sle.is_cancelled == 0)
+			)
+			.where(timestamp_condition)
+	).run(as_dict=True)
+
+	if batch_details and batch_details[0].batch_qty:
+		return batch_details[0].batch_value / batch_details[0].batch_qty
+
+
 def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
-	allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True):
+	allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True, batch_no=None):
 
 	if not company:
 		company =  frappe.get_cached_value("Warehouse", warehouse, "company")
 
+	last_valuation_rate = None
+
+	# Get moving average rate of a specific batch number
+	if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
+		last_valuation_rate = frappe.db.sql("""
+			select sum(stock_value_difference) / sum(actual_qty)
+			from `tabStock Ledger Entry`
+			where
+				item_code = %s
+				AND warehouse = %s
+				AND batch_no = %s
+				AND is_cancelled = 0
+				AND NOT (voucher_no = %s AND voucher_type = %s)
+			""",
+			(item_code, warehouse, batch_no, voucher_no, voucher_type))
+
 	# Get valuation rate from last sle for the same item and warehouse
-	last_valuation_rate = frappe.db.sql("""select valuation_rate
-		from `tabStock Ledger Entry` force index (item_warehouse)
-		where
-			item_code = %s
-			AND warehouse = %s
-			AND valuation_rate >= 0
-			AND is_cancelled = 0
-			AND NOT (voucher_no = %s AND voucher_type = %s)
-		order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
+	if not last_valuation_rate or last_valuation_rate[0][0] is None:
+		last_valuation_rate = frappe.db.sql("""select valuation_rate
+			from `tabStock Ledger Entry` force index (item_warehouse)
+			where
+				item_code = %s
+				AND warehouse = %s
+				AND valuation_rate >= 0
+				AND is_cancelled = 0
+				AND NOT (voucher_no = %s AND voucher_type = %s)
+			order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
 
 	if not last_valuation_rate:
 		# Get valuation rate from last sle for the item against any warehouse
diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py
index 648d440..bdb768f 100644
--- a/erpnext/stock/tests/test_valuation.py
+++ b/erpnext/stock/tests/test_valuation.py
@@ -7,7 +7,7 @@
 
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.stock.valuation import FIFOValuation, LIFOValuation, _round_off_if_near_zero
+from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
 from erpnext.tests.utils import ERPNextTestCase
 
 qty_gen = st.floats(min_value=-1e6, max_value=1e6)
@@ -113,11 +113,11 @@
 		self.assertTotalQty(0)
 
 	def test_rounding_off_near_zero(self):
-		self.assertEqual(_round_off_if_near_zero(0), 0)
-		self.assertEqual(_round_off_if_near_zero(1), 1)
-		self.assertEqual(_round_off_if_near_zero(-1), -1)
-		self.assertEqual(_round_off_if_near_zero(-1e-8), 0)
-		self.assertEqual(_round_off_if_near_zero(1e-8), 0)
+		self.assertEqual(round_off_if_near_zero(0), 0)
+		self.assertEqual(round_off_if_near_zero(1), 1)
+		self.assertEqual(round_off_if_near_zero(-1), -1)
+		self.assertEqual(round_off_if_near_zero(-1e-8), 0)
+		self.assertEqual(round_off_if_near_zero(1e-8), 0)
 
 	def test_totals(self):
 		self.queue.add_stock(1, 10)
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 7263e39..f85a04f 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -209,13 +209,28 @@
 @frappe.whitelist()
 def get_incoming_rate(args, raise_error_if_no_rate=True):
 	"""Get Incoming Rate based on valuation method"""
-	from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
+	from erpnext.stock.stock_ledger import (
+		get_batch_incoming_rate,
+		get_previous_sle,
+		get_valuation_rate,
+	)
 	if isinstance(args, str):
 		args = json.loads(args)
 
-	in_rate = 0
+	voucher_no = args.get('voucher_no') or args.get('name')
+
+	in_rate = None
 	if (args.get("serial_no") or "").strip():
 		in_rate = get_avg_purchase_rate(args.get("serial_no"))
+	elif args.get("batch_no") and \
+			frappe.db.get_value("Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True):
+		in_rate = get_batch_incoming_rate(
+			item_code=args.get('item_code'),
+			warehouse=args.get('warehouse'),
+			batch_no=args.get("batch_no"),
+			posting_date=args.get("posting_date"),
+			posting_time=args.get("posting_time"),
+		)
 	else:
 		valuation_method = get_valuation_method(args.get("item_code"))
 		previous_sle = get_previous_sle(args)
@@ -226,12 +241,11 @@
 		elif valuation_method == 'Moving Average':
 			in_rate = previous_sle.get('valuation_rate') or 0
 
-	if not in_rate:
-		voucher_no = args.get('voucher_no') or args.get('name')
+	if in_rate is None:
 		in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'),
 			args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'),
 			currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'),
-			raise_error_if_no_rate=raise_error_if_no_rate)
+			raise_error_if_no_rate=raise_error_if_no_rate, batch_no=args.get("batch_no"))
 
 	return flt(in_rate)
 
@@ -247,7 +261,7 @@
 	"""get valuation method from item or default"""
 	val_method = frappe.db.get_value('Item', item_code, 'valuation_method', cache=True)
 	if not val_method:
-		val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO"
+		val_method = frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO"
 	return val_method
 
 def get_fifo_rate(previous_stock_queue, qty):
diff --git a/erpnext/stock/valuation.py b/erpnext/stock/valuation.py
index ee9477e..e2bd1ad 100644
--- a/erpnext/stock/valuation.py
+++ b/erpnext/stock/valuation.py
@@ -34,7 +34,7 @@
 			total_qty += flt(qty)
 			total_value += flt(qty) * flt(rate)
 
-		return _round_off_if_near_zero(total_qty), _round_off_if_near_zero(total_value)
+		return round_off_if_near_zero(total_qty), round_off_if_near_zero(total_value)
 
 	def __repr__(self):
 		return str(self.state)
@@ -136,7 +136,7 @@
 			fifo_bin = self.queue[index]
 			if qty >= fifo_bin[QTY]:
 				# consume current bin
-				qty = _round_off_if_near_zero(qty - fifo_bin[QTY])
+				qty = round_off_if_near_zero(qty - fifo_bin[QTY])
 				to_consume = self.queue.pop(index)
 				consumed_bins.append(list(to_consume))
 
@@ -148,7 +148,7 @@
 					break
 			else:
 				# qty found in current bin consume it and exit
-				fifo_bin[QTY] = _round_off_if_near_zero(fifo_bin[QTY] - qty)
+				fifo_bin[QTY] = round_off_if_near_zero(fifo_bin[QTY] - qty)
 				consumed_bins.append([qty, fifo_bin[RATE]])
 				qty = 0
 
@@ -231,7 +231,7 @@
 			stock_bin = self.stack[index]
 			if qty >= stock_bin[QTY]:
 				# consume current bin
-				qty = _round_off_if_near_zero(qty - stock_bin[QTY])
+				qty = round_off_if_near_zero(qty - stock_bin[QTY])
 				to_consume = self.stack.pop(index)
 				consumed_bins.append(list(to_consume))
 
@@ -243,14 +243,14 @@
 					break
 			else:
 				# qty found in current bin consume it and exit
-				stock_bin[QTY] = _round_off_if_near_zero(stock_bin[QTY] - qty)
+				stock_bin[QTY] = round_off_if_near_zero(stock_bin[QTY] - qty)
 				consumed_bins.append([qty, stock_bin[RATE]])
 				qty = 0
 
 		return consumed_bins
 
 
-def _round_off_if_near_zero(number: float, precision: int = 7) -> float:
+def round_off_if_near_zero(number: float, precision: int = 7) -> float:
 	"""Rounds off the number to zero only if number is close to zero for decimal
 	specified in precision. Precision defaults to 7.
 	"""
diff --git a/erpnext/tests/test_zform_loads.py b/erpnext/tests/test_zform_loads.py
new file mode 100644
index 0000000..b6fb636
--- /dev/null
+++ b/erpnext/tests/test_zform_loads.py
@@ -0,0 +1,30 @@
+""" dumb test to check all function calls on known form loads """
+
+import unittest
+
+import frappe
+from frappe.desk.form.load import getdoc
+
+
+class TestFormLoads(unittest.TestCase):
+
+	def test_load(self):
+		erpnext_modules = frappe.get_all("Module Def", filters={"app_name": "erpnext"}, pluck="name")
+		doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0, "module": ("in", erpnext_modules)}, pluck="name")
+
+		for doctype in doctypes:
+			last_doc = frappe.db.get_value(doctype, {}, "name", order_by="modified desc")
+			if not last_doc:
+				continue
+			with self.subTest(msg=f"Loading {doctype} - {last_doc}", doctype=doctype, last_doc=last_doc):
+				try:
+					# reset previous response
+					frappe.response = frappe._dict({"docs":[]})
+					frappe.response.docinfo = None
+
+					getdoc(doctype, last_doc)
+				except Exception as e:
+					self.fail(f"Failed to load {doctype} - {last_doc}: {e}")
+
+				self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}")
+				self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}")
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index cf73564..b882b9d 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -1597,6 +1597,7 @@
 Middle Income,Mittleres Einkommen,
 Middle Name,Zweiter Vorname,
 Middle Name (Optional),Weiterer Vorname (optional),
+Milestonde,Meilenstein,
 Min Amt can not be greater than Max Amt,Min. Amt kann nicht größer als Max. Amt sein,
 Min Qty can not be greater than Max Qty,Mindestmenge kann nicht größer als Maximalmenge sein,
 Minimum Lead Age (Days),Mindest Lead-Alter (in Tagen),
@@ -3730,7 +3731,7 @@
 Edit Details,Details bearbeiten,
 Edit Profile,Profil bearbeiten,
 Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Bei Straßentransport ist entweder die GST-Transporter-ID oder die Fahrzeug-Nr. Erforderlich,
-Email,Email,
+Email,E-Mail,
 Email Campaigns,E-Mail-Kampagnen,
 Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft,
 Employee Tax and Benefits,Mitarbeitersteuern und -leistungen,
@@ -6486,7 +6487,7 @@
 Send Emails At,Die E-Mails senden um,
 Reminder,Erinnerung,
 Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer,
-email,Email,
+email,E-Mail,
 Parent Department,Elternabteilung,
 Leave Block List,Urlaubssperrenliste,
 Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.",
diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html
index e72bfc8..95ba8f7 100644
--- a/erpnext/www/lms/macros/hero.html
+++ b/erpnext/www/lms/macros/hero.html
@@ -11,7 +11,7 @@
 			{% if frappe.session.user == 'Guest' %}
 			<a id="signup" class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
 			{% elif not has_access %}
-			<button id="enroll" class="btn btn-primary btn-lg" onclick="enroll()" disabled>{{_('Enroll')}}</button>
+			<button id="enroll" class="btn btn-primary btn-lg" onclick="enroll()">{{_('Enroll')}}</button>
 			{% endif %}
 		</p>
 	</div>
@@ -20,34 +20,35 @@
 <script type="text/javascript">
 	frappe.ready(() => {
 		btn = document.getElementById('enroll');
-		if (btn) btn.disabled = false;
 	})
 
 	function enroll() {
 		let params = frappe.utils.get_query_params()
 
 		let btn = document.getElementById('enroll');
-		btn.disbaled = true;
-		btn.innerText = __('Enrolling...')
 
 		let opts = {
 			method: 'erpnext.education.utils.enroll_in_program',
 			args: {
 				program_name: params.program
-			}
+			},
+			freeze: true,
+			freeze_message: __('Enrolling...')
 		}
 
 		frappe.call(opts).then(res => {
 			let success_dialog = new frappe.ui.Dialog({
 				title: __('Success'),
+				primary_action_label: __('View Program Content'),
+				primary_action: function() {
+					window.location.reload();
+				},
 				secondary_action: function() {
-					window.location.reload()
+					window.location.reload();
 				}
 			})
-			success_dialog.set_message(__('You have successfully enrolled for the program '));
-			success_dialog.$message.show()
 			success_dialog.show();
-			btn.disbaled = false;
+			success_dialog.set_message(__('You have successfully enrolled for the program '));
 		})
 	}
 </script>