Merge branch 'develop' into actual-qty-total-js-reactive
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index f2a696d..a0c0ecc 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2648,6 +2648,7 @@
 		# reset
 		einvoice_settings = frappe.get_doc("E Invoice Settings")
 		einvoice_settings.enable = 0
+		einvoice_settings.save()
 		frappe.flags.country = country
 
 	def test_einvoice_json(self):
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 78645e0..46013bb 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2451,11 +2451,21 @@
 			parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
 		)
 
-	def validate_quantity(child_item, d):
-		if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
+	def validate_quantity(child_item, new_data):
+		if not flt(new_data.get("qty")):
+			frappe.throw(
+				_("Row # {0}: Quantity for Item {1} cannot be zero").format(
+					new_data.get("idx"), frappe.bold(new_data.get("item_code"))
+				),
+				title=_("Invalid Qty"),
+			)
+
+		if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
 			frappe.throw(_("Cannot set quantity less than delivered quantity"))
 
-		if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty):
+		if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(
+			child_item.received_qty
+		):
 			frappe.throw(_("Cannot set quantity less than received quantity"))
 
 	data = json.loads(trans_items)
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 19b4d68..b590177 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -9,7 +9,7 @@
 from frappe.email.inbox import link_communication_to_document
 from frappe.model.mapper import get_mapped_doc
 from frappe.query_builder import DocType
-from frappe.utils import cint, cstr, flt, get_fullname
+from frappe.utils import cint, flt, get_fullname
 
 from erpnext.crm.utils import add_link_in_communication, copy_comments
 from erpnext.setup.utils import get_exchange_rate
@@ -215,20 +215,20 @@
 
 			if self.party_name and self.opportunity_from == "Customer":
 				if self.contact_person:
-					opts.description = "Contact " + cstr(self.contact_person)
+					opts.description = f"Contact {self.contact_person}"
 				else:
-					opts.description = "Contact customer " + cstr(self.party_name)
+					opts.description = f"Contact customer {self.party_name}"
 			elif self.party_name and self.opportunity_from == "Lead":
 				if self.contact_display:
-					opts.description = "Contact " + cstr(self.contact_display)
+					opts.description = f"Contact {self.contact_display}"
 				else:
-					opts.description = "Contact lead " + cstr(self.party_name)
+					opts.description = f"Contact lead {self.party_name}"
 
 			opts.subject = opts.description
-			opts.description += ". By : " + cstr(self.contact_by)
+			opts.description += f". By : {self.contact_by}"
 
 			if self.to_discuss:
-				opts.description += " To Discuss : " + cstr(self.to_discuss)
+				opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}"
 
 			super(Opportunity, self).add_calendar_event(opts, force)
 
diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py
index 481c7f1..4a18e94 100644
--- a/erpnext/crm/doctype/opportunity/test_opportunity.py
+++ b/erpnext/crm/doctype/opportunity/test_opportunity.py
@@ -4,7 +4,7 @@
 import unittest
 
 import frappe
-from frappe.utils import now_datetime, random_string, today
+from frappe.utils import add_days, now_datetime, random_string, today
 
 from erpnext.crm.doctype.lead.lead import make_customer
 from erpnext.crm.doctype.lead.test_lead import make_lead
@@ -97,6 +97,22 @@
 		self.assertEqual(quotation_comment_count, 4)
 		self.assertEqual(quotation_communication_count, 4)
 
+	def test_render_template_for_to_discuss(self):
+		doc = make_opportunity(with_items=0, opportunity_from="Lead")
+		doc.contact_by = "test@example.com"
+		doc.contact_date = add_days(today(), days=2)
+		doc.to_discuss = "{{ doc.name }} test data"
+		doc.save()
+
+		event = frappe.get_all(
+			"Event Participants",
+			fields=["parent"],
+			filters={"reference_doctype": doc.doctype, "reference_docname": doc.name},
+		)
+
+		event_description = frappe.db.get_value("Event", event[0].parent, "description")
+		self.assertTrue(doc.name in event_description)
+
 
 def make_opportunity_from_lead():
 	new_lead_email_id = "new{}@example.com".format(random_string(5))
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d5b1592..1fef240 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -369,4 +369,5 @@
 erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
 erpnext.patches.v14_0.discount_accounting_separation
 erpnext.patches.v14_0.delete_employee_transfer_property_doctype
-erpnext.patches.v13_0.create_accounting_dimensions_in_orders
\ No newline at end of file
+erpnext.patches.v13_0.create_accounting_dimensions_in_orders
+erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py b/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py
new file mode 100644
index 0000000..a4d7012
--- /dev/null
+++ b/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+
+
+def execute():
+	dn = frappe.qb.DocType("Delivery Note")
+	dn_item = frappe.qb.DocType("Delivery Note Item")
+
+	dn_list = (
+		frappe.qb.from_(dn)
+		.inner_join(dn_item)
+		.on(dn.name == dn_item.parent)
+		.select(dn.name)
+		.where(dn.docstatus == 1)
+		.where(dn.is_return == 1)
+		.where(dn.per_billed < 100)
+		.where(dn_item.returned_qty > 0)
+		.run(as_dict=True)
+	)
+
+	frappe.qb.update(dn_item).inner_join(dn).on(dn.name == dn_item.parent).set(
+		dn_item.returned_qty, 0
+	).where(dn.is_return == 1).where(dn_item.returned_qty > 0).run()
+
+	for d in dn_list:
+		dn_doc = frappe.get_doc("Delivery Note", d.get("name"))
+		dn_doc.run_method("update_billing_status")
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index c4b27a5..ea56d07 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -204,6 +204,29 @@
 				};
 				add_custom_button(__("Cancel E-Way Bill"), action);
 			}
+
+			if (irn && !irn_cancelled) {
+				const action = () => {
+					const dialog = frappe.msgprint({
+						title: __("Generate QRCode"),
+						message: __("Generate and attach QR Code using IRN?"),
+						primary_action: {
+							action: function() {
+								frappe.call({
+									method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
+									args: { doctype, docname: name },
+									freeze: true,
+									callback: () => frm.reload_doc() || dialog.hide(),
+									error: () => dialog.hide()
+								});
+							}
+						},
+						primary_action_label: __('Yes')
+					});
+					dialog.show();
+				};
+				add_custom_button(__("Generate QRCode"), action);
+			}
 		}
 	});
 };
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 53d3211..ed1002a 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -167,7 +167,12 @@
 			title=_("Not Allowed"),
 		)
 
-	invoice_type = "CRN" if invoice.is_return else "INV"
+	if invoice.is_return:
+		invoice_type = "CRN"
+	elif invoice.is_debit_note:
+		invoice_type = "DBN"
+	else:
+		invoice_type = "INV"
 
 	invoice_name = invoice.name
 	invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy")
@@ -794,6 +799,7 @@
 		self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
 		self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi"
 		self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
+		self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
 
 	def set_invoice(self):
 		self.invoice = None
@@ -857,8 +863,8 @@
 		return res
 
 	def auto_refresh_token(self):
-		self.fetch_auth_token()
 		self.token_auto_refreshed = True
+		self.fetch_auth_token()
 
 	def log_request(self, url, headers, data, res):
 		headers.update({"password": self.credentials.password})
@@ -998,6 +1004,37 @@
 
 		return failed
 
+	def fetch_and_attach_qrcode_from_irn(self):
+		qrcode = self.get_qrcode_from_irn(self.invoice.irn)
+		if qrcode:
+			qrcode_file = self.create_qr_code_file(qrcode)
+			frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url)
+			frappe.msgprint(_("QR Code attached to the invoice"), alert=True)
+		else:
+			frappe.msgprint(_("QR Code not found for the IRN"), alert=True)
+
+	def get_qrcode_from_irn(self, irn):
+		import requests
+
+		headers = self.get_headers()
+		headers.update({"width": "215", "height": "215", "imgtype": "jpg", "irn": irn})
+
+		try:
+			# using requests.get instead of make_request to avoid parsing the response
+			res = requests.get(self.get_qrcode_url, headers=headers)
+			self.log_request(self.get_qrcode_url, headers, None, None)
+			if res.status_code == 200:
+				return res.content
+			else:
+				raise RequestFailed(str(res.content, "utf-8"))
+
+		except RequestFailed as e:
+			self.raise_error(errors=str(e))
+
+		except Exception:
+			log_error()
+			self.raise_error()
+
 	def get_irn_details(self, irn):
 		headers = self.get_headers()
 
@@ -1198,8 +1235,6 @@
 		return errors
 
 	def raise_error(self, raise_exception=False, errors=None):
-		if errors is None:
-			errors = []
 		title = _("E Invoice Request Failed")
 		if errors:
 			frappe.throw(errors, title=title, as_list=1)
@@ -1240,13 +1275,18 @@
 
 	def attach_qrcode_image(self):
 		qrcode = self.invoice.signed_qr_code
-		doctype = self.invoice.doctype
-		docname = self.invoice.name
-		filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__")
 
 		qr_image = io.BytesIO()
 		url = qrcreate(qrcode, error="L")
 		url.png(qr_image, scale=2, quiet_zone=1)
+		qrcode_file = self.create_qr_code_file(qr_image.getvalue())
+		self.invoice.qrcode_image = qrcode_file.file_url
+
+	def create_qr_code_file(self, qr_image):
+		doctype = self.invoice.doctype
+		docname = self.invoice.name
+		filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__")
+
 		_file = frappe.get_doc(
 			{
 				"doctype": "File",
@@ -1255,12 +1295,12 @@
 				"attached_to_name": docname,
 				"attached_to_field": "qrcode_image",
 				"is_private": 0,
-				"content": qr_image.getvalue(),
+				"content": qr_image,
 			}
 		)
 		_file.save()
 		frappe.db.commit()
-		self.invoice.qrcode_image = _file.file_url
+		return _file
 
 	def update_invoice(self):
 		self.invoice.flags.ignore_validate_update_after_submit = True
@@ -1306,6 +1346,12 @@
 
 
 @frappe.whitelist()
+def generate_qrcode(doctype, docname):
+	gsp_connector = GSPConnector(doctype, docname)
+	gsp_connector.fetch_and_attach_qrcode_from_irn()
+
+
+@frappe.whitelist()
 def generate_eway_bill(doctype, docname, **kwargs):
 	gsp_connector = GSPConnector(doctype, docname)
 	gsp_connector.generate_eway_bill(**kwargs)
diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
index 091c20c..e10df2a 100644
--- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
+++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
@@ -238,4 +238,5 @@
 			"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}],
 		},
 		"type": "bar",
+		"fieldtype": "Currency",
 	}
diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py
index 4e0758d..4d71ce7 100644
--- a/erpnext/selling/report/quotation_trends/quotation_trends.py
+++ b/erpnext/selling/report/quotation_trends/quotation_trends.py
@@ -54,4 +54,5 @@
 		},
 		"type": "line",
 		"lineOptions": {"regionFill": 1},
+		"fieldtype": "Currency",
 	}
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py
index 1a2476a..9d7d806 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.py
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.py
@@ -415,3 +415,8 @@
 		else:
 			labels = [d.get("label") for d in self.columns[1 : length - 1]]
 		self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
+
+		if self.filters["value_quantity"] == "Value":
+			self.chart["fieldtype"] = "Currency"
+		else:
+			self.chart["fieldtype"] = "Float"
diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py
index 719f1c5..18f448c 100644
--- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py
+++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py
@@ -51,4 +51,5 @@
 		},
 		"type": "line",
 		"lineOptions": {"regionFill": 1},
+		"fieldtype": "Currency",
 	}
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index f97e7ca..0738bfb 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -962,6 +962,44 @@
 
 		automatically_fetch_payment_terms(enable=0)
 
+	def test_returned_qty_in_return_dn(self):
+		# SO ---> SI ---> DN
+		#                 |
+		#                 |---> DN(Partial Sales Return) ---> SI(Credit Note)
+		#                 |
+		#                 |---> DN(Partial Sales Return) ---> SI(Credit Note)
+
+		from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
+		from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+
+		so = make_sales_order(qty=10)
+		si = make_sales_invoice(so.name)
+		si.insert()
+		si.submit()
+		dn = make_delivery_note(si.name)
+		dn.insert()
+		dn.submit()
+		self.assertEqual(dn.items[0].returned_qty, 0)
+		self.assertEqual(dn.per_billed, 100)
+
+		from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+
+		dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
+		si1 = make_sales_invoice(dn1.name)
+		si1.insert()
+		si1.submit()
+		dn1.reload()
+		self.assertEqual(dn1.items[0].returned_qty, 0)
+		self.assertEqual(dn1.per_billed, 100)
+
+		dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
+		si2 = make_sales_invoice(dn2.name)
+		si2.insert()
+		si2.submit()
+		dn2.reload()
+		self.assertEqual(dn2.items[0].returned_qty, 0)
+		self.assertEqual(dn2.per_billed, 100)
+
 
 def create_delivery_note(**args):
 	dn = frappe.new_doc("Delivery Note")
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index e2eb2a4..2d7abc8 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -737,7 +737,9 @@
    "depends_on": "returned_qty",
    "fieldname": "returned_qty",
    "fieldtype": "Float",
-   "label": "Returned Qty in Stock UOM"
+   "label": "Returned Qty in Stock UOM",
+   "no_copy": 1,
+   "read_only": 1
   },
   {
    "fieldname": "incoming_rate",
@@ -778,7 +780,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-03-31 18:36:24.671913",
+ "modified": "2022-05-02 12:09:39.610075",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Delivery Note Item",
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 5850ec7..4e2fc83 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
@@ -1183,6 +1183,42 @@
 		backdated.cancel()
 		self.assertEqual([1], ordered_qty_after_transaction())
 
+	def test_timestamp_clash(self):
+
+		item = make_item().name
+		warehouse = "_Test Warehouse - _TC"
+
+		reciept = make_stock_entry(
+			item_code=item,
+			to_warehouse=warehouse,
+			qty=100,
+			rate=10,
+			posting_date="2021-01-01",
+			posting_time="01:00:00",
+		)
+
+		consumption = make_stock_entry(
+			item_code=item,
+			from_warehouse=warehouse,
+			qty=50,
+			posting_date="2021-01-01",
+			posting_time="02:00:00.1234",  # ms are possible when submitted without editing posting time
+		)
+
+		backdated_receipt = make_stock_entry(
+			item_code=item,
+			to_warehouse=warehouse,
+			qty=100,
+			posting_date="2021-01-01",
+			rate=10,
+			posting_time="02:00:00",  # same posting time as consumption but ms part stripped
+		)
+
+		try:
+			backdated_receipt.cancel()
+		except Exception as e:
+			self.fail("Double processing of qty for clashing timestamp.")
+
 
 def create_repack_entry(**args):
 	args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index e7b89b1..9088eb8 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -31,6 +31,7 @@
 
 	def tearDown(self):
 		frappe.local.future_sle = {}
+		frappe.flags.pop("dont_execute_stock_reposts", None)
 
 	def test_reco_for_fifo(self):
 		self._test_reco_sle_gle("FIFO")
@@ -384,6 +385,7 @@
 		-------------------------------------------
 		Var		| Doc	|	Qty	| Balance
 		-------------------------------------------
+		PR5     | PR    |   10  |  10   (posting date: today-4) [backdated]
 		SR5		| Reco	|	0	|	8	(posting date: today-4) [backdated]
 		PR1		| PR	|	10	|	18	(posting date: today-3)
 		PR2		| PR	|	1	|	19	(posting date: today-2)
@@ -393,6 +395,14 @@
 		item_code = make_item().name
 		warehouse = "_Test Warehouse - _TC"
 
+		frappe.flags.dont_execute_stock_reposts = True
+
+		def assertBalance(doc, qty_after_transaction):
+			sle_balance = frappe.db.get_value(
+				"Stock Ledger Entry", {"voucher_no": doc.name, "is_cancelled": 0}, "qty_after_transaction"
+			)
+			self.assertEqual(sle_balance, qty_after_transaction)
+
 		pr1 = make_purchase_receipt(
 			item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
 		)
@@ -402,62 +412,37 @@
 		pr3 = make_purchase_receipt(
 			item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate()
 		)
-
-		pr1_balance = frappe.db.get_value(
-			"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
-		)
-		pr3_balance = frappe.db.get_value(
-			"Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction"
-		)
-		self.assertEqual(pr1_balance, 10)
-		self.assertEqual(pr3_balance, 12)
+		assertBalance(pr1, 10)
+		assertBalance(pr3, 12)
 
 		# post backdated stock reco in between
 		sr4 = create_stock_reconciliation(
 			item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1)
 		)
-		pr3_balance = frappe.db.get_value(
-			"Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction"
-		)
-		self.assertEqual(pr3_balance, 7)
+		assertBalance(pr3, 7)
 
 		# post backdated stock reco at the start
 		sr5 = create_stock_reconciliation(
 			item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4)
 		)
-		pr1_balance = frappe.db.get_value(
-			"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
+		assertBalance(pr1, 18)
+		assertBalance(pr2, 19)
+		assertBalance(sr4, 6)  # check if future stock reco is unaffected
+
+		# Make a backdated receipt and check only entries till first SR are affected
+		pr5 = make_purchase_receipt(
+			item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -5)
 		)
-		pr2_balance = frappe.db.get_value(
-			"Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction"
-		)
-		sr4_balance = frappe.db.get_value(
-			"Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction"
-		)
-		self.assertEqual(pr1_balance, 18)
-		self.assertEqual(pr2_balance, 19)
-		self.assertEqual(sr4_balance, 6)  # check if future stock reco is unaffected
+		assertBalance(pr5, 10)
+		# check if future stock reco is unaffected
+		assertBalance(sr4, 6)
+		assertBalance(sr5, 8)
 
 		# cancel backdated stock reco and check future impact
 		sr5.cancel()
-		pr1_balance = frappe.db.get_value(
-			"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
-		)
-		pr2_balance = frappe.db.get_value(
-			"Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction"
-		)
-		sr4_balance = frappe.db.get_value(
-			"Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction"
-		)
-		self.assertEqual(pr1_balance, 10)
-		self.assertEqual(pr2_balance, 11)
-		self.assertEqual(sr4_balance, 6)  # check if future stock reco is unaffected
-
-		# teardown
-		sr4.cancel()
-		pr3.cancel()
-		pr2.cancel()
-		pr1.cancel()
+		assertBalance(pr1, 10)
+		assertBalance(pr2, 11)
+		assertBalance(sr4, 6)  # check if future stock reco is unaffected
 
 	@change_settings("Stock Settings", {"allow_negative_stock": 0})
 	def test_backdated_stock_reco_future_negative_stock(self):
@@ -563,7 +548,6 @@
 
 		# repost will make this test useless, qty should update in realtime without reposts
 		frappe.flags.dont_execute_stock_reposts = True
-		self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
 
 		item_code = make_item().name
 		warehouse = "_Test Warehouse - _TC"
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/__init__.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/__init__.py
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js
new file mode 100644
index 0000000..0b8f496
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js
@@ -0,0 +1,53 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+const DIFFERNCE_FIELD_NAMES = [
+	"fifo_qty_diff",
+	"fifo_value_diff",
+];
+
+frappe.query_reports["FIFO Queue vs Qty After Transaction Comparison"] = {
+	"filters": [
+		{
+			"fieldname": "item_code",
+			"fieldtype": "Link",
+			"label": "Item",
+			"options": "Item",
+			get_query: function() {
+				return {
+					filters: {is_stock_item: 1, has_serial_no: 0}
+				}
+			}
+		},
+		{
+			"fieldname": "item_group",
+			"fieldtype": "Link",
+			"label": "Item Group",
+			"options": "Item Group",
+		},
+		{
+			"fieldname": "warehouse",
+			"fieldtype": "Link",
+			"label": "Warehouse",
+			"options": "Warehouse",
+		},
+		{
+			"fieldname": "from_date",
+			"fieldtype": "Date",
+			"label": "From Posting Date",
+		},
+		{
+			"fieldname": "to_date",
+			"fieldtype": "Date",
+			"label": "From Posting Date",
+		}
+	],
+	formatter (value, row, column, data, default_formatter) {
+		value = default_formatter(value, row, column, data);
+		if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
+			value = "<span style='color:red'>" + value + "</span>";
+		}
+		return value;
+	},
+};
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.json b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.json
new file mode 100644
index 0000000..5e958aa
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.json
@@ -0,0 +1,27 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2022-05-11 04:09:13.460652",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "abc",
+ "modified": "2022-05-11 04:09:20.232177",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "FIFO Queue vs Qty After Transaction Comparison",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "FIFO Queue vs Qty After Transaction Comparison",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Administrator"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py
new file mode 100644
index 0000000..9e14033
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py
@@ -0,0 +1,212 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import json
+
+import frappe
+from frappe import _
+from frappe.utils import flt
+from frappe.utils.nestedset import get_descendants_of
+
+SLE_FIELDS = (
+	"name",
+	"item_code",
+	"warehouse",
+	"posting_date",
+	"posting_time",
+	"creation",
+	"voucher_type",
+	"voucher_no",
+	"actual_qty",
+	"qty_after_transaction",
+	"stock_queue",
+	"batch_no",
+	"stock_value",
+	"valuation_rate",
+)
+
+
+def execute(filters=None):
+	columns = get_columns()
+	data = get_data(filters)
+	return columns, data
+
+
+def get_data(filters):
+	if not any([filters.warehouse, filters.item_code, filters.item_group]):
+		frappe.throw(_("Any one of following filters required: warehouse, Item Code, Item Group"))
+	sles = get_stock_ledger_entries(filters)
+	return find_first_bad_queue(sles)
+
+
+def get_stock_ledger_entries(filters):
+
+	sle_filters = {"is_cancelled": 0}
+
+	if filters.warehouse:
+		children = get_descendants_of("Warehouse", filters.warehouse)
+		sle_filters["warehouse"] = ("in", children + [filters.warehouse])
+
+	if filters.item_code:
+		sle_filters["item_code"] = filters.item_code
+	elif filters.get("item_group"):
+		item_group = filters.get("item_group")
+		children = get_descendants_of("Item Group", item_group)
+		item_group_filter = {"item_group": ("in", children + [item_group])}
+		sle_filters["item_code"] = (
+			"in",
+			frappe.get_all("Item", filters=item_group_filter, pluck="name", order_by=None),
+		)
+
+	if filters.from_date:
+		sle_filters["posting_date"] = (">=", filters.from_date)
+	if filters.to_date:
+		sle_filters["posting_date"] = ("<=", filters.to_date)
+
+	return frappe.get_all(
+		"Stock Ledger Entry",
+		fields=SLE_FIELDS,
+		filters=sle_filters,
+		order_by="timestamp(posting_date, posting_time), creation",
+	)
+
+
+def find_first_bad_queue(sles):
+	item_warehouse_sles = {}
+	for sle in sles:
+		item_warehouse_sles.setdefault((sle.item_code, sle.warehouse), []).append(sle)
+
+	data = []
+
+	for _item_wh, sles in item_warehouse_sles.items():
+		for idx, sle in enumerate(sles):
+			queue = json.loads(sle.stock_queue or "[]")
+
+			sle.fifo_queue_qty = 0.0
+			sle.fifo_stock_value = 0.0
+			for qty, rate in queue:
+				sle.fifo_queue_qty += flt(qty)
+				sle.fifo_stock_value += flt(qty) * flt(rate)
+
+			sle.fifo_qty_diff = sle.qty_after_transaction - sle.fifo_queue_qty
+			sle.fifo_value_diff = sle.stock_value - sle.fifo_stock_value
+
+			if sle.batch_no:
+				sle.use_batchwise_valuation = frappe.db.get_value(
+					"Batch", sle.batch_no, "use_batchwise_valuation", cache=True
+				)
+
+			if abs(sle.fifo_qty_diff) > 0.001 or abs(sle.fifo_value_diff) > 0.1:
+				if idx:
+					data.append(sles[idx - 1])
+				data.append(sle)
+				data.append({})
+				break
+
+	return data
+
+
+def get_columns():
+	return [
+		{
+			"fieldname": "name",
+			"fieldtype": "Link",
+			"label": _("Stock Ledger Entry"),
+			"options": "Stock Ledger Entry",
+		},
+		{
+			"fieldname": "item_code",
+			"fieldtype": "Link",
+			"label": _("Item Code"),
+			"options": "Item",
+		},
+		{
+			"fieldname": "warehouse",
+			"fieldtype": "Link",
+			"label": _("Warehouse"),
+			"options": "Warehouse",
+		},
+		{
+			"fieldname": "posting_date",
+			"fieldtype": "Data",
+			"label": _("Posting Date"),
+		},
+		{
+			"fieldname": "posting_time",
+			"fieldtype": "Data",
+			"label": _("Posting Time"),
+		},
+		{
+			"fieldname": "creation",
+			"fieldtype": "Data",
+			"label": _("Creation"),
+		},
+		{
+			"fieldname": "voucher_type",
+			"fieldtype": "Link",
+			"label": _("Voucher Type"),
+			"options": "DocType",
+		},
+		{
+			"fieldname": "voucher_no",
+			"fieldtype": "Dynamic Link",
+			"label": _("Voucher No"),
+			"options": "voucher_type",
+		},
+		{
+			"fieldname": "batch_no",
+			"fieldtype": "Link",
+			"label": _("Batch"),
+			"options": "Batch",
+		},
+		{
+			"fieldname": "use_batchwise_valuation",
+			"fieldtype": "Check",
+			"label": _("Batchwise Valuation"),
+		},
+		{
+			"fieldname": "actual_qty",
+			"fieldtype": "Float",
+			"label": _("Qty Change"),
+		},
+		{
+			"fieldname": "qty_after_transaction",
+			"fieldtype": "Float",
+			"label": _("(A) Qty After Transaction"),
+		},
+		{
+			"fieldname": "stock_queue",
+			"fieldtype": "Data",
+			"label": _("FIFO/LIFO Queue"),
+		},
+		{
+			"fieldname": "fifo_queue_qty",
+			"fieldtype": "Float",
+			"label": _("(C) Total qty in queue"),
+		},
+		{
+			"fieldname": "fifo_qty_diff",
+			"fieldtype": "Float",
+			"label": _("A - C"),
+		},
+		{
+			"fieldname": "stock_value",
+			"fieldtype": "Float",
+			"label": _("(D) Balance Stock Value"),
+		},
+		{
+			"fieldname": "fifo_stock_value",
+			"fieldtype": "Float",
+			"label": _("(E) Balance Stock Value in Queue"),
+		},
+		{
+			"fieldname": "fifo_value_diff",
+			"fieldtype": "Float",
+			"label": _("D - E"),
+		},
+		{
+			"fieldname": "valuation_rate",
+			"fieldtype": "Float",
+			"label": _("(H) Valuation Rate"),
+		},
+	]
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 837c4a6..ed0e2fc 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
@@ -111,17 +111,17 @@
 		},
 		{
 			"fieldname": "posting_date",
-			"fieldtype": "Date",
+			"fieldtype": "Data",
 			"label": _("Posting Date"),
 		},
 		{
 			"fieldname": "posting_time",
-			"fieldtype": "Time",
+			"fieldtype": "Data",
 			"label": _("Posting Time"),
 		},
 		{
 			"fieldname": "creation",
-			"fieldtype": "Datetime",
+			"fieldtype": "Data",
 			"label": _("Creation"),
 		},
 		{
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index 55b9104..d118d8e 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -65,6 +65,8 @@
 	("Delayed Item Report", {"based_on": "Delivery Note"}),
 	("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
 	("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
+	("FIFO Queue vs Qty After Transaction Comparison", {"warehouse": "_Test Warehouse - _TC"}),
+	("FIFO Queue vs Qty After Transaction Comparison", {"item_group": "All Item Groups"}),
 ]
 
 OPTIONAL_FILTERS = {
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 7e5c231..4789b52 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -1303,6 +1303,8 @@
 	datetime_limit_condition = ""
 	qty_shift = args.actual_qty
 
+	args["time_format"] = "%H:%i:%s"
+
 	# find difference/shift in qty caused by stock reconciliation
 	if args.voucher_type == "Stock Reconciliation":
 		qty_shift = get_stock_reco_qty_shift(args)
@@ -1315,7 +1317,7 @@
 		datetime_limit_condition = get_datetime_limit_condition(detail)
 
 	frappe.db.sql(
-		"""
+		f"""
 		update `tabStock Ledger Entry`
 		set qty_after_transaction = qty_after_transaction + {qty_shift}
 		where
@@ -1323,16 +1325,10 @@
 			and warehouse = %(warehouse)s
 			and voucher_no != %(voucher_no)s
 			and is_cancelled = 0
-			and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
-				or (
-					timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
-					and creation > %(creation)s
-				)
-			)
+			and timestamp(posting_date, time_format(posting_time, %(time_format)s))
+				> timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
 		{datetime_limit_condition}
-		""".format(
-			qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition
-		),
+		""",
 		args,
 	)
 
@@ -1383,6 +1379,7 @@
 					and creation > %(creation)s
 				)
 			)
+		order by timestamp(posting_date, posting_time) asc, creation asc
 		limit 1
 	""",
 		args,
diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv
index 6ca3344..fb56ff6 100644
--- a/erpnext/translations/ru.csv
+++ b/erpnext/translations/ru.csv
@@ -1357,7 +1357,7 @@
 "Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates.","Цена товара отображается несколько раз на основе Прайс-листа, Поставщика / Клиента, Валюты, Предмет, UOM, Кол-во и Даты.",
 Item Price updated for {0} in Price List {1},Цена продукта {0} обновлена в прайс-листе {1},
 Item Row {0}: {1} {2} does not exist in above '{1}' table,Элемент Row {0}: {1} {2} не существует в таблице «{1}»,
-Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable,"Строка налога {0} должен иметь счет типа Налога, Доход, Расходов или Облагаемый налогом,"
+Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable,"Строка налога {0} должен иметь счет типа Налога, Доход, Расходов или Облагаемый налогом",
 Item Template,Шаблон продукта,
 Item Variant Settings,Параметры модификации продкута,
 Item Variant {0} already exists with same attributes,Модификация продукта {0} с этими атрибутами уже существует,