Merge pull request #29334 from nextchamp-saqib/grouped-asset-purchase

diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index cfa42f6..55e3853 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2192,9 +2192,9 @@
 		asset.load_from_db()
 
 		expected_values = [
-			["2020-06-30", 1311.48, 1311.48],
-			["2021-06-30", 20000.0, 21311.48],
-			["2021-09-30", 5041.1, 26352.58]
+			["2020-06-30", 1366.12, 1366.12],
+			["2021-06-30", 20000.0, 21366.12],
+			["2021-09-30", 5041.1, 26407.22]
 		]
 
 		for i, schedule in enumerate(asset.schedules):
@@ -2242,12 +2242,12 @@
 		asset.load_from_db()
 
 		expected_values = [
-			["2020-06-30", 1311.48, 1311.48, True],
-			["2021-06-30", 20000.0, 21311.48, True],
-			["2022-06-30", 20000.0, 41311.48, False],
-			["2023-06-30", 20000.0, 61311.48, False],
-			["2024-06-30",  20000.0, 81311.48,  False],
-			["2025-06-06",  18688.52,  100000.0, False]
+			["2020-06-30", 1366.12, 1366.12, True],
+			["2021-06-30", 20000.0, 21366.12, True],
+			["2022-06-30", 20000.0, 41366.12, False],
+			["2023-06-30", 20000.0, 61366.12, False],
+			["2024-06-30",  20000.0, 81366.12,  False],
+			["2025-06-06",  18633.88,  100000.0, False]
 		]
 
 		for i, schedule in enumerate(asset.schedules):
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index e23b033..ac64a95 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -256,8 +256,9 @@
 
 				# For first row
 				if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
+					from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
 					depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
-						self.available_for_use_date, finance_book.depreciation_start_date)
+						from_date, finance_book.depreciation_start_date)
 
 					# For first depr schedule date will be the start date
 					# so monthly schedule date is calculated by removing month difference between use date and start date
@@ -388,7 +389,9 @@
 
 		if from_date:
 			return from_date
-		return self.available_for_use_date
+
+		# since depr for available_for_use_date is not yet booked
+		return add_days(self.available_for_use_date, -1)
 
 	# if it returns True, depreciation_amount will not be equal for the first and last rows
 	def check_is_pro_rata(self, row):
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index dca67b3..b9545f4 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -230,9 +230,9 @@
 		self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
 
 		expected_gle = (
-			("_Test Accumulated Depreciations - _TC", 20392.16, 0.0),
+			("_Test Accumulated Depreciations - _TC", 20490.2, 0.0),
 			("_Test Fixed Asset - _TC", 0.0, 100000.0),
-			("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0),
+			("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0),
 			("Debtors - _TC", 25000.0, 0.0)
 		)
 
@@ -514,10 +514,10 @@
 		)
 
 		expected_schedules = [
-			["2030-12-31", 27534.25, 27534.25],
-			["2031-12-31", 30000.0, 57534.25],
-			["2032-12-31", 30000.0, 87534.25],
-			["2033-01-30", 2465.75, 90000.0]
+			['2030-12-31', 27616.44, 27616.44],
+			['2031-12-31', 30000.0, 57616.44],
+			['2032-12-31', 30000.0, 87616.44],
+			['2033-01-30', 2383.56, 90000.0]
 		]
 
 		schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -567,10 +567,10 @@
 		self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
 
 		expected_schedules = [
-			["2030-12-31", 28493.15, 28493.15],
-			["2031-12-31", 35753.43, 64246.58],
-			["2032-12-31", 17876.71, 82123.29],
-			["2033-06-06", 5376.71, 87500.0]
+			['2030-12-31', 28630.14, 28630.14],
+			['2031-12-31', 35684.93, 64315.07],
+			['2032-12-31', 17842.47, 82157.54],
+			['2033-06-06', 5342.46, 87500.0]
 		]
 
 		schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -603,10 +603,10 @@
 		self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
 
 		expected_schedules = [
-			["2030-12-31", 11780.82, 11780.82],
-			["2031-12-31", 44109.59, 55890.41],
-			["2032-12-31", 22054.8, 77945.21],
-			["2033-07-12", 9554.79, 87500.0]
+			["2030-12-31", 11849.32, 11849.32],
+			["2031-12-31", 44075.34, 55924.66],
+			["2032-12-31", 22037.67, 77962.33],
+			["2033-07-12", 9537.67, 87500.0]
 		]
 
 		schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -644,7 +644,7 @@
 		asset = create_asset(
 			item_code = "Macbook Pro",
 			calculate_depreciation = 1,
-			available_for_use_date = getdate("2019-12-31"),
+			available_for_use_date = getdate("2020-01-01"),
 			total_number_of_depreciations = 3,
 			expected_value_after_useful_life = 10000,
 			depreciation_start_date = getdate("2020-07-01"),
@@ -655,7 +655,7 @@
 			["2020-07-01", 15000, 15000],
 			["2021-07-01", 30000, 45000],
 			["2022-07-01", 30000, 75000],
-			["2022-12-31", 15000, 90000]
+			["2023-01-01", 15000, 90000]
 		]
 
 		for i, schedule in enumerate(asset.schedules):
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index f22669b..b97432e 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -256,11 +256,7 @@
 		for d in self.items:
 			if not d.batch_no: continue
 
-			serial_nos = [sr.name for sr in frappe.get_all("Serial No",
-				{'batch_no': d.batch_no, 'status': 'Inactive'})]
-
-			if serial_nos:
-				frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
+			frappe.db.set_value("Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None)
 
 			d.batch_no = None
 			d.db_set("batch_no", None)
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
index 4d3c3f4..6e727e5 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
@@ -4,6 +4,7 @@
 import unittest
 
 import frappe
+from frappe.utils import format_date
 from frappe.utils.data import add_days, formatdate, today
 
 from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import (
@@ -82,6 +83,13 @@
 
 		#checks if visit status is back updated in schedule
 		self.assertTrue(ms.schedules[1].completion_status, "Partially Completed")
+		self.assertEqual(format_date(visit.mntc_date), format_date(ms.schedules[1].actual_date))
+
+		#checks if visit status is updated on cancel
+		visit.cancel()
+		ms.reload()
+		self.assertTrue(ms.schedules[1].completion_status, "Pending")
+		self.assertEqual(ms.schedules[1].actual_date, None)
 
 	def test_serial_no_filters(self):
 		# Without serial no. set in schedule -> returns None
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
index d5d8753..6fe2466 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
@@ -4,7 +4,7 @@
 
 import frappe
 from frappe import _
-from frappe.utils import get_datetime
+from frappe.utils import format_date, get_datetime
 
 from erpnext.utilities.transaction_base import TransactionBase
 
@@ -28,20 +28,24 @@
 			if item_ref:
 				start_date, end_date = frappe.db.get_value('Maintenance Schedule Item', item_ref, ['start_date', 'end_date'])
 				if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(self.mntc_date) > get_datetime(end_date):
-					frappe.throw(_("Date must be between {0} and {1}").format(start_date, end_date))
+					frappe.throw(_("Date must be between {0} and {1}")
+						.format(format_date(start_date), format_date(end_date)))
+
 
 	def validate(self):
 		self.validate_serial_no()
 		self.validate_maintenance_date()
 		self.validate_purpose_table()
 
-	def update_completion_status(self):
+	def update_status_and_actual_date(self, cancel=False):
+		status = "Pending"
+		actual_date = None
+		if not cancel:
+			status = self.completion_status
+			actual_date = self.mntc_date
 		if self.maintenance_schedule_detail:
-			frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', self.completion_status)
-
-	def update_actual_date(self):
-		if self.maintenance_schedule_detail:
-			frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', self.mntc_date)
+			frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', status)
+			frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', actual_date)
 
 	def update_customer_issue(self, flag):
 		if not self.maintenance_schedule:
@@ -102,12 +106,12 @@
 	def on_submit(self):
 		self.update_customer_issue(1)
 		frappe.db.set(self, 'status', 'Submitted')
-		self.update_completion_status()
-		self.update_actual_date()
+		self.update_status_and_actual_date()
 
 	def on_cancel(self):
 		self.check_if_last_visit()
 		frappe.db.set(self, 'status', 'Cancelled')
+		self.update_status_and_actual_date(cancel=True)
 
 	def on_update(self):
 		pass
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index f1f0eb6..83bab06 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -318,6 +318,7 @@
 erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
 erpnext.patches.v14_0.migrate_crm_settings
 erpnext.patches.v13_0.rename_ksa_qr_field
+erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
 erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
 erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
 erpnext.patches.v13_0.update_tax_category_for_rcm
diff --git a/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py
new file mode 100644
index 0000000..30b7592
--- /dev/null
+++ b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py
@@ -0,0 +1,11 @@
+import frappe
+
+
+def execute():
+	sr_item = frappe.qb.DocType("Stock Reconciliation Item")
+
+	(frappe.qb
+		.update(sr_item)
+		.set(sr_item.current_serial_no, None)
+		.where(sr_item.current_qty == 0)
+	).run()
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index c4ddc9e..428370c 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -6,7 +6,7 @@
 
 
 import frappe
-from frappe.utils import add_days, flt, nowdate, nowtime, random_string
+from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
 
 from erpnext.accounts.utils import get_stock_and_account_balance
 from erpnext.stock.doctype.item.test_item import create_item
@@ -439,8 +439,8 @@
 		self.assertRaises(frappe.ValidationError, sr.submit)
 
 	def test_serial_no_cancellation(self):
-
 		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
 		item = create_item("Stock-Reco-Serial-Item-9", is_stock_item=1)
 		if not item.has_serial_no:
 			item.has_serial_no = 1
@@ -466,6 +466,31 @@
 		self.assertEqual(len(active_sr_no), 10)
 
 
+	def test_serial_no_creation_and_inactivation(self):
+		item = create_item("_TestItemCreatedWithStockReco", is_stock_item=1)
+		if not item.has_serial_no:
+			item.has_serial_no = 1
+			item.save()
+
+		item_code = item.name
+		warehouse = "_Test Warehouse - _TC"
+
+		sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse,
+				serial_no="SR-CREATED-SR-NO", qty=1, do_not_submit=True, rate=100)
+		sr.save()
+		self.assertEqual(cstr(sr.items[0].current_serial_no), "")
+		sr.submit()
+
+		active_sr_no = frappe.get_all("Serial No",
+				filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
+		self.assertEqual(len(active_sr_no), 1)
+
+		sr.cancel()
+		active_sr_no = frappe.get_all("Serial No",
+				filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
+		self.assertEqual(len(active_sr_no), 0)
+
+
 def create_batch_item_with_batch(item_name, batch_id):
 	batch_item_doc = create_item(item_name, is_stock_item=1)
 	if not batch_item_doc.has_batch_no:
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 1a846f0..f620c18 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -103,7 +103,7 @@
 			serial_nos = get_serial_nos_data_after_transactions(args)
 
 			return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
-				if last_entry else (0.0, 0.0, 0.0))
+				if last_entry else (0.0, 0.0, None))
 		else:
 			return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0)
 	else: