Merge pull request #26252 from deepeshgarg007/loan_security_pledge_on_cancel

fix: Cancellation of Loan Security Pledges
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 35097b9..8196cff 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -11,7 +11,7 @@
 
 import erpnext
 from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
-from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
+from erpnext.accounts.utils import get_fiscal_year
 from erpnext.controllers.accounts_controller import AccountsController
 from erpnext.stock import get_warehouse_account_map
 from erpnext.stock.stock_ledger import get_valuation_rate
@@ -497,9 +497,6 @@
 		})
 		if future_sle_exists(args):
 			create_repost_item_valuation_entry(args)
-		elif not is_reposting_pending():
-			check_if_stock_and_account_balance_synced(self.posting_date,
-				self.company, self.doctype, self.name)
 
 @frappe.whitelist()
 def make_quality_inspections(doctype, docname, items):
diff --git a/erpnext/hr/doctype/attendance/attendance.js b/erpnext/hr/doctype/attendance/attendance.js
index c3c3cb8..7964078 100644
--- a/erpnext/hr/doctype/attendance/attendance.js
+++ b/erpnext/hr/doctype/attendance/attendance.js
@@ -11,5 +11,5 @@
 cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
 	return{
 		query: "erpnext.controllers.queries.employee_query"
-	}	
+	}
 }
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index f3b8a79..3412675 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -15,6 +15,7 @@
 		validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
 		self.validate_attendance_date()
 		self.validate_duplicate_record()
+		self.validate_employee_status()
 		self.check_leave_record()
 
 	def validate_attendance_date(self):
@@ -38,6 +39,10 @@
 			frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
 				frappe.bold(self.employee), frappe.bold(self.attendance_date)))
 
+	def validate_employee_status(self):
+		if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
+			frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
+
 	def check_leave_record(self):
 		leave_record = frappe.db.sql("""
 			select leave_type, half_day, half_day_date
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 0c7eafe..9a3bac0 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -21,6 +21,9 @@
 						label: __('For Employee'),
 						fieldtype: 'Link',
 						options: 'Employee',
+						get_query: () => {
+							return {query: "erpnext.controllers.queries.employee_query"}
+						},
 						reqd: 1,
 						onchange: function() {
 							dialog.set_df_property("unmarked_days", "hidden", 1);
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
index 4c31bd0..f19a1b0 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
@@ -13,7 +13,7 @@
 
 	refresh: function(frm) {
 		erpnext.hide_company();
-		if (frm.doc.customer && frm.doc.docstatus === 1) {
+		if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
 			frm.add_custom_button(__("Sales Order"), function() {
 				frappe.model.open_mapped_doc({
 					method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
index 0330e5c..a63fc4d 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
@@ -1,4 +1,5 @@
 {
+ "actions": [],
  "autoname": "naming_series:",
  "creation": "2018-05-24 07:18:08.256060",
  "doctype": "DocType",
@@ -79,6 +80,7 @@
    "reqd": 1
   },
   {
+   "allow_on_submit": 1,
    "fieldname": "to_date",
    "fieldtype": "Date",
    "label": "To Date",
@@ -129,8 +131,10 @@
    "label": "Terms and Conditions Details"
   }
  ],
+ "index_web_pages_for_search": 1,
  "is_submittable": 1,
- "modified": "2019-11-18 19:37:37.151686",
+ "links": [],
+ "modified": "2021-06-29 00:30:30.621636",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Blanket Order",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 3e85560..c58f017 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1,7 +1,8 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
-from __future__ import unicode_literals
+from typing import List
+from collections import deque
 import frappe, erpnext
 from frappe.utils import cint, cstr, flt, today
 from frappe import _
@@ -16,14 +17,85 @@
 
 import functools
 
-from six import string_types
-
 from operator import itemgetter
 
 form_grid_templates = {
 	"items": "templates/form_grid/item_grid.html"
 }
 
+
+class BOMTree:
+	"""Full tree representation of a BOM"""
+
+	# specifying the attributes to save resources
+	# ref: https://docs.python.org/3/reference/datamodel.html#slots
+	__slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
+
+	def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None:
+		self.name = name  # name of node, BOM number if is_bom else item_code
+		self.child_items: List["BOMTree"] = []  # list of child items
+		self.is_bom = is_bom   # true if the node is a BOM and not a leaf item
+		self.item_code: str = None  # item_code associated with node
+		self.qty = qty  # required unit quantity to make one unit of parent item.
+		self.exploded_qty = exploded_qty  # total exploded qty required for making root of tree.
+		if not self.is_bom:
+			self.item_code = self.name
+		else:
+			self.__create_tree()
+
+	def __create_tree(self):
+		bom = frappe.get_cached_doc("BOM", self.name)
+		self.item_code = bom.item
+
+		for item in bom.get("items", []):
+			qty = item.qty / bom.quantity  # quantity per unit
+			exploded_qty = self.exploded_qty * qty
+			if item.bom_no:
+				child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
+				self.child_items.append(child)
+			else:
+				self.child_items.append(
+					BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty)
+				)
+
+	def level_order_traversal(self) -> List["BOMTree"]:
+		"""Get level order traversal of tree.
+		E.g. for following tree the traversal will return list of nodes in order from top to bottom.
+		BOM:
+			- SubAssy1
+				- item1
+				- item2
+			- SubAssy2
+				- item3
+			- item4
+
+		returns = [SubAssy1, item1, item2, SubAssy2, item3, item4]
+		"""
+		traversal = []
+		q = deque()
+		q.append(self)
+
+		while q:
+			node = q.popleft()
+
+			for child in node.child_items:
+				traversal.append(child)
+				q.append(child)
+
+		return traversal
+
+	def __str__(self) -> str:
+		return (
+			f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}"
+			f" exploded_qty: {self.exploded_qty}"
+		)
+
+	def __repr__(self, level: int = 0) -> str:
+		rep = "┃  " * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n"
+		for child in self.child_items:
+			rep += child.__repr__(level=level + 1)
+		return rep
+
 class BOM(WebsiteGenerator):
 	website = frappe._dict(
 		# page_title_field = "item_name",
@@ -152,7 +224,7 @@
 		if not args:
 			args = frappe.form_dict.get('args')
 
-		if isinstance(args, string_types):
+		if isinstance(args, str):
 			import json
 			args = json.loads(args)
 
@@ -600,6 +672,11 @@
 				if not d.batch_size or d.batch_size <= 0:
 					d.batch_size = 1
 
+	def get_tree_representation(self) -> BOMTree:
+		"""Get a complete tree representation preserving order of child items."""
+		return BOMTree(self.name)
+
+
 def get_bom_item_rate(args, bom_doc):
 	if bom_doc.rm_cost_as_per == 'Valuation Rate':
 		rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 1f443fb..57a5458 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -2,14 +2,13 @@
 # License: GNU General Public License v3. See license.txt
 
 
-from __future__ import unicode_literals
+from collections import deque
 import unittest
 import frappe
 from frappe.utils import cstr, flt
 from frappe.test_runner import make_test_records
 from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
 from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
-from six import string_types
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
 from erpnext.tests.test_subcontracting import set_backflush_based_on
@@ -227,11 +226,88 @@
 		supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
 		self.assertEqual(bom_items, supplied_items)
 
+	def test_bom_tree_representation(self):
+		bom_tree = {
+			"Assembly": {
+				"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
+				"SubAssembly2": {"ChildPart3": {}},
+				"SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
+				"ChildPart5": {},
+				"ChildPart6": {},
+				"SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
+			}
+		}
+		parent_bom = create_nested_bom(bom_tree, prefix="")
+		created_tree = parent_bom.get_tree_representation()
+
+		reqd_order = level_order_traversal(bom_tree)[1:] # skip first item
+		created_order = created_tree.level_order_traversal()
+
+		self.assertEqual(len(reqd_order), len(created_order))
+
+		for reqd_item, created_item in zip(reqd_order, created_order):
+			self.assertEqual(reqd_item, created_item.item_code)
+
+
 def get_default_bom(item_code="_Test FG Item 2"):
 	return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
 
+
+
+
+def level_order_traversal(node):
+	traversal = []
+	q = deque()
+	q.append(node)
+
+	while q:
+		node = q.popleft()
+
+		for node_name, subtree in node.items():
+			traversal.append(node_name)
+			q.append(subtree)
+
+	return traversal
+
+def create_nested_bom(tree, prefix="_Test bom "):
+	""" Helper function to create a simple nested bom from tree describing item names. (along with required items)
+	"""
+
+	def create_items(bom_tree):
+		for item_code, subtree in bom_tree.items():
+			bom_item_code = prefix + item_code
+			if not frappe.db.exists("Item", bom_item_code):
+				frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert()
+			create_items(subtree)
+	create_items(tree)
+
+	def dfs(tree, node):
+		"""naive implementation for searching right subtree"""
+		for node_name, subtree in tree.items():
+			if node_name == node:
+				return subtree
+			else:
+				result = dfs(subtree, node)
+				if result is not None:
+					return result
+
+	order_of_creating_bom = reversed(level_order_traversal(tree))
+
+	for item in order_of_creating_bom:
+		child_items = dfs(tree, item)
+		if child_items:
+			bom_item_code = prefix + item
+			bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
+			for child_item in child_items.keys():
+				bom.append("items", {"item_code": prefix + child_item})
+			bom.insert()
+			bom.submit()
+
+	return bom  # parent bom is last bom
+
+
 def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None):
-	if warehouse_list and isinstance(warehouse_list, string_types):
+	if warehouse_list and isinstance(warehouse_list, str):
 		warehouse_list = [warehouse_list]
 
 	if not warehouse_list:
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index cb1ee92..68de0b2 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -389,17 +389,12 @@
 		ste.submit()
 		stock_entries.append(ste)
 
-		job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
+		job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc')
 		self.assertEqual(len(job_cards), len(bom.operations))
 
 		for i, job_card in enumerate(job_cards):
 			doc = frappe.get_doc("Job Card", job_card)
-			doc.append("time_logs", {
-				"from_time": add_to_date(None, i),
-				"hours": 1,
-				"to_time": add_to_date(None, i + 1),
-				"completed_qty": doc.for_quantity
-			})
+			doc.time_logs[0].completed_qty = 1
 			doc.submit()
 
 		ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index e343ed2..180815d 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -1,7 +1,6 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
-from __future__ import unicode_literals
 import frappe
 import json
 import math
@@ -30,9 +29,6 @@
 class SerialNoQtyError(frappe.ValidationError):
 	pass
 
-form_grid_templates = {
-	"operations": "templates/form_grid/work_order_grid.html"
-}
 
 class WorkOrder(Document):
 	def onload(self):
@@ -472,46 +468,47 @@
 
 	def set_work_order_operations(self):
 		"""Fetch operations from BOM and set in 'Work Order'"""
-		self.set('operations', [])
 
+		def _get_operations(bom_no, qty=1):
+			return frappe.db.sql(
+					f"""select
+						operation, description, workstation, idx,
+						base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins,
+						"Pending" as status, parent as bom, batch_size, sequence_id
+					from
+						`tabBOM Operation`
+					where
+						parent = %s order by idx
+					""", bom_no, as_dict=1)
+
+
+		self.set('operations', [])
 		if not self.bom_no:
 			return
 
-		if self.use_multi_level_bom:
-			bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree()
+		operations = []
+		if not self.use_multi_level_bom:
+			bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
+			operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
 		else:
-			bom_list = [self.bom_no]
+			bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation()
+			bom_traversal = list(reversed(bom_tree.level_order_traversal()))
+			bom_traversal.append(bom_tree) # add operation on top level item last
 
-		operations = frappe.db.sql("""
-			select
-				operation, description, workstation, idx,
-				base_hour_rate as hour_rate, time_in_mins,
-				"Pending" as status, parent as bom, batch_size, sequence_id
-			from
-				`tabBOM Operation`
-			where
-				 parent in (%s) order by idx
-		"""	% ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1)
+			for d in bom_traversal:
+				if d.is_bom:
+					operations.extend(_get_operations(d.name, qty=d.exploded_qty))
+
+			for correct_index, operation in enumerate(operations, start=1):
+				operation.idx = correct_index
+
 
 		self.set('operations', operations)
-
-		if self.use_multi_level_bom and self.get('operations') and self.get('items'):
-			raw_material_operations = [d.operation for d in self.get('items')]
-			operations = [d.operation for d in self.get('operations')]
-
-			for operation in raw_material_operations:
-				if operation not in operations:
-					self.append('operations', {
-						'operation': operation
-					})
-
 		self.calculate_time()
 
 	def calculate_time(self):
-		bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
-
 		for d in self.get("operations"):
-			d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size))
+			d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
 
 		self.calculate_operating_cost()
 
diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
index 6d8fb80..f7b8787 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -2,7 +2,6 @@
  "actions": [],
  "creation": "2014-10-16 14:35:41.950175",
  "doctype": "DocType",
- "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
   "details",
@@ -49,6 +48,7 @@
   {
    "fieldname": "bom",
    "fieldtype": "Link",
+   "in_list_view": 1,
    "label": "BOM",
    "no_copy": 1,
    "options": "BOM",
@@ -68,6 +68,7 @@
    "fieldtype": "Column Break"
   },
   {
+   "columns": 1,
    "description": "Operation completed for how many finished goods?",
    "fieldname": "completed_qty",
    "fieldtype": "Float",
@@ -77,6 +78,7 @@
    "read_only": 1
   },
   {
+   "columns": 1,
    "default": "Pending",
    "fieldname": "status",
    "fieldtype": "Select",
@@ -119,6 +121,7 @@
    "fieldtype": "Column Break"
   },
   {
+   "columns": 1,
    "description": "in Minutes",
    "fieldname": "time_in_mins",
    "fieldtype": "Float",
@@ -205,7 +208,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-01-12 14:48:31.061286",
+ "modified": "2021-06-24 14:36:12.835543",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Work Order Operation",
@@ -214,4 +217,4 @@
  "sort_field": "modified",
  "sort_order": "DESC",
  "track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index e71d81f..5c7c0a3 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -459,6 +459,7 @@
 			where
 				t1.name = t2.employee
 				and t2.docstatus = 1
+				and t1.status != 'Inactive'
 		%s order by t2.from_date desc
 		""" % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True)
 
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index ac4ed5e..a01db80 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -48,37 +48,54 @@
 	},
 
 	get_items: function(frm) {
-		frappe.prompt({label:"Warehouse", fieldname: "warehouse", fieldtype:"Link", options:"Warehouse", reqd: 1,
+		let fields = [{
+			label: 'Warehouse', fieldname: 'warehouse', fieldtype: 'Link', options: 'Warehouse', reqd: 1,
 			"get_query": function() {
 				return {
 					"filters": {
 						"company": frm.doc.company,
 					}
-				}
-			}},
-			function(data) {
-				frappe.call({
-					method:"erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
-					args: {
-						warehouse: data.warehouse,
-						posting_date: frm.doc.posting_date,
-						posting_time: frm.doc.posting_time,
-						company:frm.doc.company
-					},
-					callback: function(r) {
-						var items = [];
-						frm.clear_table("items");
-						for(var i=0; i< r.message.length; i++) {
-							var d = frm.add_child("items");
-							$.extend(d, r.message[i]);
-							if(!d.qty) d.qty = null;
-							if(!d.valuation_rate) d.valuation_rate = null;
-						}
-						frm.refresh_field("items");
-					}
-				});
+				};
 			}
-		, __("Get Items"), __("Update"));
+		}, {
+			label: "Item Code", fieldname: "item_code", fieldtype: "Link", options: "Item",
+			"get_query": function() {
+				return {
+					"filters": {
+						"disabled": 0,
+					}
+				};
+			}
+		}];
+
+		frappe.prompt(fields, function(data) {
+			frappe.call({
+				method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
+				args: {
+					warehouse: data.warehouse,
+					posting_date: frm.doc.posting_date,
+					posting_time: frm.doc.posting_time,
+					company: frm.doc.company,
+					item_code: data.item_code
+				},
+				callback: function(r) {
+					frm.clear_table("items");
+					for (var i=0; i<r.message.length; i++) {
+						var d = frm.add_child("items");
+						$.extend(d, r.message[i]);
+
+						if (!d.qty) {
+							d.qty = 0;
+						}
+
+						if (!d.valuation_rate) {
+							d.valuation_rate = 0;
+						}
+					}
+					frm.refresh_field("items");
+				}
+			});
+		}, __("Get Items"), __("Update"));
 	},
 
 	posting_date: function(frm) {
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 2b51c1a..e646600 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -481,45 +481,99 @@
 			self._cancel()
 
 @frappe.whitelist()
-def get_items(warehouse, posting_date, posting_time, company):
+def get_items(warehouse, posting_date, posting_time, company, item_code=None):
+	items = [frappe._dict({
+		'item_code': item_code,
+		'warehouse': warehouse
+	})]
+
+	if not item_code:
+		items = get_items_for_stock_reco(warehouse, company)
+
+	res = []
+	itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code)
+
+	for d in items:
+		if d.item_code in itemwise_batch_data:
+			stock_bal = get_stock_balance(d.item_code, d.warehouse,
+				posting_date, posting_time, with_valuation_rate=True)
+
+			for row in itemwise_batch_data.get(d.item_code):
+				args = get_item_data(row, row.qty, stock_bal[1])
+				res.append(args)
+		else:
+			stock_bal = get_stock_balance(d.item_code, d.warehouse, posting_date, posting_time,
+				with_valuation_rate=True , with_serial_no=cint(d.has_serial_no))
+
+			args = get_item_data(d, stock_bal[0], stock_bal[1],
+				stock_bal[2] if cint(d.has_serial_no) else '')
+
+			res.append(args)
+
+	return res
+
+def get_items_for_stock_reco(warehouse, company):
 	lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
 	items = frappe.db.sql("""
-		select i.name, i.item_name, bin.warehouse, i.has_serial_no
+		select i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no
 		from tabBin bin, tabItem i
-		where i.name=bin.item_code and i.disabled=0 and i.is_stock_item = 1
-		and i.has_variants = 0 and i.has_batch_no = 0
-		and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse)
-	""", (lft, rgt))
+		where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1
+		and i.has_variants = 0 and exists(
+			select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse
+		)
+	""", (lft, rgt), as_dict=1)
 
 	items += frappe.db.sql("""
-		select i.name, i.item_name, id.default_warehouse, i.has_serial_no
+		select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no
 		from tabItem i, `tabItem Default` id
 		where i.name = id.parent
 			and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse)
-			and i.is_stock_item = 1 and i.has_batch_no = 0
-			and i.has_variants = 0 and i.disabled = 0 and id.company=%s
+			and i.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s
 		group by i.name
-	""", (lft, rgt, company))
+	""", (lft, rgt, company), as_dict=1)
 
-	res = []
-	for d in set(items):
-		stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time,
-			with_valuation_rate=True , with_serial_no=cint(d[3]))
+	return items
 
-		if frappe.db.get_value("Item", d[0], "disabled") == 0:
-			res.append({
-				"item_code": d[0],
-				"warehouse": d[2],
-				"qty": stock_bal[0],
-				"item_name": d[1],
-				"valuation_rate": stock_bal[1],
-				"current_qty": stock_bal[0],
-				"current_valuation_rate": stock_bal[1],
-				"current_serial_no": stock_bal[2] if cint(d[3]) else '',
-				"serial_no": stock_bal[2] if cint(d[3]) else ''
-			})
+def get_item_data(row, qty, valuation_rate, serial_no=None):
+	return {
+		'item_code': row.item_code,
+		'warehouse': row.warehouse,
+		'qty': qty,
+		'item_name': row.item_name,
+		'valuation_rate': valuation_rate,
+		'current_qty': qty,
+		'current_valuation_rate': valuation_rate,
+		'current_serial_no': serial_no,
+		'serial_no': serial_no,
+		'batch_no': row.get('batch_no')
+	}
 
-	return res
+def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
+	from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute
+	itemwise_batch_data = {}
+
+	filters = frappe._dict({
+		'warehouse': warehouse,
+		'from_date': posting_date,
+		'to_date': posting_date,
+		'company': company
+	})
+
+	if item_code:
+		filters.item_code = item_code
+
+	columns, data = execute(filters)
+
+	for row in data:
+		itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({
+			'item_code': row[0],
+			'warehouse': warehouse,
+			'qty': row[8],
+			'item_name': row[1],
+			'batch_no': row[4]
+		}))
+
+	return itemwise_batch_data
 
 @frappe.whitelist()
 def get_stock_balance_for(item_code, warehouse,
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index c64084f..ca174a3 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -438,6 +438,13 @@
 @frappe.whitelist()
 def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None):
 	out = {}
+
+	if item_tax_templates is None:
+		item_tax_templates = {}
+	
+	if item_rates is None:
+		item_rates = {}
+
 	if isinstance(item_codes, (str,)):
 		item_codes = json.loads(item_codes)
 
@@ -453,7 +460,7 @@
 
 		out[item_code[1]] = {}
 		item = frappe.get_cached_doc("Item", item_code[0])
-		args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]}
+		args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])}
 
 		if item_tax_templates:
 			args.update({"item_tax_template": item_tax_templates.get(item_code[1])})
diff --git a/erpnext/stock/report/incorrect_stock_value_report/__init__.py b/erpnext/stock/report/incorrect_stock_value_report/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/__init__.py
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js
new file mode 100644
index 0000000..ff42480
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js
@@ -0,0 +1,36 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Incorrect Stock Value Report"] = {
+	"filters": [
+		{
+			"label": __("Company"),
+			"fieldname": "company",
+			"fieldtype": "Link",
+			"options": "Company",
+			"reqd": 1,
+			"default": frappe.defaults.get_user_default("Company")
+		},
+		{
+			"label": __("Account"),
+			"fieldname": "account",
+			"fieldtype": "Link",
+			"options": "Account",
+			get_query: function() {
+				var company = frappe.query_report.get_filter_value('company');
+				return {
+					filters: {
+						"account_type": "Stock",
+						"company": company
+					}
+				}
+			}
+		},
+		{
+			"label": __("From Date"),
+			"fieldname": "from_date",
+			"fieldtype": "Date"
+		}
+	]
+};
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json
new file mode 100644
index 0000000..a7e9f20
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-06-22 15:35:05.148177",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-06-22 15:35:05.148177",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Incorrect Stock Value Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Incorrect Stock Value Report",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Stock User"
+  },
+  {
+   "role": "Accounts Manager"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
new file mode 100644
index 0000000..a724387
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import erpnext
+from frappe import _
+from six import iteritems
+from frappe.utils import add_days, today, getdate
+from erpnext.stock.utils import get_stock_value_on
+from erpnext.accounts.utils import get_stock_and_account_balance
+
+def execute(filters=None):
+	if not erpnext.is_perpetual_inventory_enabled(filters.company):
+		frappe.throw(_("Perpetual inventory required for the company {0} to view this report.")
+			.format(filters.company))
+
+	data = get_data(filters)
+	columns = get_columns(filters)
+
+	return columns, data
+
+def get_unsync_date(filters):
+	date = filters.from_date
+	if not date:
+		date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""")
+		date = date[0][0]
+
+	if not date:
+		return
+
+	while getdate(date) < getdate(today()):
+		account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date,
+			company=filters.company, account = filters.account)
+
+		if abs(account_bal - stock_bal) > 0.1:
+			return date
+
+		date = add_days(date, 1)
+
+def get_data(report_filters):
+	from_date = get_unsync_date(report_filters)
+
+	if not from_date:
+		return []
+
+	result = []
+
+	voucher_wise_dict = {}
+	data = frappe.db.sql('''
+			SELECT
+				name, posting_date, posting_time, voucher_type, voucher_no,
+				stock_value_difference, stock_value, warehouse, item_code
+			FROM
+				`tabStock Ledger Entry`
+			WHERE
+				posting_date
+				= %s and company = %s
+				and is_cancelled = 0
+			ORDER BY timestamp(posting_date, posting_time) asc, creation asc
+		''', (from_date, report_filters.company), as_dict=1)
+
+	for d in data:
+		voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d)
+
+	closing_date = add_days(from_date, -1)
+	for key, stock_data in iteritems(voucher_wise_dict):
+		prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1])
+		for data in stock_data:
+			expected_stock_value = prev_stock_value + data.stock_value_difference
+			if abs(data.stock_value - expected_stock_value) > 0.1:
+				data.difference_value = abs(data.stock_value - expected_stock_value)
+				data.expected_stock_value = expected_stock_value
+				result.append(data)
+
+	return result
+
+def get_columns(filters):
+	return [
+		{
+			"label": _("Stock Ledger ID"),
+			"fieldname": "name",
+			"fieldtype": "Link",
+			"options": "Stock Ledger Entry",
+			"width": "80"
+		},
+		{
+			"label": _("Posting Date"),
+			"fieldname": "posting_date",
+			"fieldtype": "Date"
+		},
+		{
+			"label": _("Posting Time"),
+			"fieldname": "posting_time",
+			"fieldtype": "Time"
+		},
+		{
+			"label": _("Voucher Type"),
+			"fieldname": "voucher_type",
+			"width": "110"
+		},
+		{
+			"label": _("Voucher No"),
+			"fieldname": "voucher_no",
+			"fieldtype": "Dynamic Link",
+			"options": "voucher_type",
+			"width": "110"
+		},
+		{
+			"label": _("Item Code"),
+			"fieldname": "item_code",
+			"fieldtype": "Link",
+			"options": "Item",
+			"width": "110"
+		},
+		{
+			"label": _("Warehouse"),
+			"fieldname": "warehouse",
+			"fieldtype": "Link",
+			"options": "Warehouse",
+			"width": "110"
+		},
+		{
+			"label": _("Expected Stock Value"),
+			"fieldname": "expected_stock_value",
+			"fieldtype": "Currency",
+			"width": "150"
+		},
+		{
+			"label": _("Stock Value"),
+			"fieldname": "stock_value",
+			"fieldtype": "Currency",
+			"width": "120"
+		},
+		{
+			"label": _("Difference Value"),
+			"fieldname": "difference_value",
+			"fieldtype": "Currency",
+			"width": "150"
+		}
+	]
\ No newline at end of file