feat: asset activity (#36391)

* feat: asset activity

* chore: add more actions to asset activity

* chore: fix failing test due to timestamp mismatch error

* chore: rewriting asset activity messages

* chore: add report and add it to workspace

* chore: show user in list view
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index f5ee228..b0cc8ca 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -32,6 +32,7 @@
 	reset_depreciation_schedule,
 	reverse_depreciation_entry_made_after_disposal,
 )
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.controllers.accounts_controller import validate_account_head
 from erpnext.controllers.selling_controller import SellingController
 from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
@@ -1176,12 +1177,13 @@
 							self.get("posting_date"),
 						)
 						asset.db_set("disposal_date", None)
+						add_asset_activity(asset.name, _("Asset returned"))
 
 						if asset.calculate_depreciation:
 							posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
 							reverse_depreciation_entry_made_after_disposal(asset, posting_date)
 							notes = _(
-								"This schedule was created when Asset {0} was returned after being sold through Sales Invoice {1}."
+								"This schedule was created when Asset {0} was returned through Sales Invoice {1}."
 							).format(
 								get_link_to_form(asset.doctype, asset.name),
 								get_link_to_form(self.doctype, self.get("name")),
@@ -1209,6 +1211,7 @@
 							self.get("posting_date"),
 						)
 						asset.db_set("disposal_date", self.posting_date)
+						add_asset_activity(asset.name, _("Asset sold"))
 
 					for gle in fixed_asset_gl_entries:
 						gle["against"] = self.customer
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 2eb5f3d..befb524 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -534,13 +534,18 @@
    "link_fieldname": "asset"
   },
   {
+   "group": "Activity",
+   "link_doctype": "Asset Activity",
+   "link_fieldname": "asset"
+  },
+  {
    "group": "Journal Entry",
    "link_doctype": "Journal Entry",
    "link_fieldname": "reference_name",
    "table_fieldname": "accounts"
   }
  ],
- "modified": "2023-07-28 15:47:01.137996",
+ "modified": "2023-07-28 20:12:44.819616",
  "modified_by": "Administrator",
  "module": "Assets",
  "name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 9efa18b..252a3dd 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -25,6 +25,7 @@
 	get_depreciation_accounts,
 	get_disposal_account_and_cost_center,
 )
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
 from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
 	cancel_asset_depr_schedules,
@@ -59,7 +60,7 @@
 		self.make_asset_movement()
 		if not self.booked_fixed_asset and self.validate_make_gl_entry():
 			self.make_gl_entries()
-		if not self.split_from:
+		if self.calculate_depreciation and not self.split_from:
 			asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self)
 			convert_draft_asset_depr_schedules_into_active(self)
 			if asset_depr_schedules_names:
@@ -71,6 +72,7 @@
 						"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
 					).format(asset_depr_schedules_links)
 				)
+		add_asset_activity(self.name, _("Asset submitted"))
 
 	def on_cancel(self):
 		self.validate_cancellation()
@@ -81,9 +83,10 @@
 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
 		make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
 		self.db_set("booked_fixed_asset", 0)
+		add_asset_activity(self.name, _("Asset cancelled"))
 
 	def after_insert(self):
-		if not self.split_from:
+		if self.calculate_depreciation and not self.split_from:
 			asset_depr_schedules_names = make_draft_asset_depr_schedules(self)
 			asset_depr_schedules_links = get_comma_separated_links(
 				asset_depr_schedules_names, "Asset Depreciation Schedule"
@@ -93,6 +96,16 @@
 					"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
 				).format(asset_depr_schedules_links)
 			)
+		if not frappe.db.exists(
+			{
+				"doctype": "Asset Activity",
+				"asset": self.name,
+			}
+		):
+			add_asset_activity(self.name, _("Asset created"))
+
+	def after_delete(self):
+		add_asset_activity(self.name, _("Asset deleted"))
 
 	def validate_asset_and_reference(self):
 		if self.purchase_invoice or self.purchase_receipt:
@@ -903,6 +916,13 @@
 		},
 	)
 
+	add_asset_activity(
+		asset.name,
+		_("Asset updated after being split into Asset {0}").format(
+			get_link_to_form("Asset", new_asset_name)
+		),
+	)
+
 	for row in asset.get("finance_books"):
 		value_after_depreciation = flt(
 			(row.value_after_depreciation * remaining_qty) / asset.asset_quantity
@@ -970,6 +990,15 @@
 			(row.expected_value_after_useful_life * split_qty) / asset.asset_quantity
 		)
 
+	new_asset.insert()
+
+	add_asset_activity(
+		new_asset.name,
+		_("Asset created after being split from Asset {0}").format(
+			get_link_to_form("Asset", asset.name)
+		),
+	)
+
 	new_asset.submit()
 	new_asset.set_status()
 
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index a311bc6..0588065 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -21,6 +21,7 @@
 	get_checks_for_pl_and_bs_accounts,
 )
 from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
 	get_asset_depr_schedule_doc,
 	get_asset_depr_schedule_name,
@@ -325,6 +326,8 @@
 	frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
 	asset.set_status("Scrapped")
 
+	add_asset_activity(asset_name, _("Asset scrapped"))
+
 	frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name))
 
 
@@ -349,6 +352,8 @@
 
 	asset.set_status()
 
+	add_asset_activity(asset_name, _("Asset restored"))
+
 
 def depreciate_asset(asset_doc, date, notes):
 	asset_doc.flags.ignore_validate_update_after_submit = True
diff --git a/erpnext/assets/doctype/asset_activity/__init__.py b/erpnext/assets/doctype/asset_activity/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/__init__.py
diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.js b/erpnext/assets/doctype/asset_activity/asset_activity.js
new file mode 100644
index 0000000..38d3434
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/asset_activity.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Asset Activity", {
+// 	refresh(frm) {
+
+// 	},
+// });
diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.json b/erpnext/assets/doctype/asset_activity/asset_activity.json
new file mode 100644
index 0000000..476fb27
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/asset_activity.json
@@ -0,0 +1,109 @@
+{
+ "actions": [],
+ "creation": "2023-07-28 12:41:13.232505",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "asset",
+  "column_break_vkdy",
+  "date",
+  "column_break_kkxv",
+  "user",
+  "section_break_romx",
+  "subject"
+ ],
+ "fields": [
+  {
+   "fieldname": "asset",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Asset",
+   "options": "Asset",
+   "print_width": "165",
+   "read_only": 1,
+   "reqd": 1,
+   "width": "165"
+  },
+  {
+   "fieldname": "column_break_vkdy",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_romx",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "subject",
+   "fieldtype": "Small Text",
+   "in_list_view": 1,
+   "label": "Subject",
+   "print_width": "518",
+   "read_only": 1,
+   "reqd": 1,
+   "width": "518"
+  },
+  {
+   "default": "now",
+   "fieldname": "date",
+   "fieldtype": "Datetime",
+   "in_list_view": 1,
+   "label": "Date",
+   "print_width": "158",
+   "read_only": 1,
+   "reqd": 1,
+   "width": "158"
+  },
+  {
+   "fieldname": "user",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "User",
+   "options": "User",
+   "print_width": "150",
+   "read_only": 1,
+   "reqd": 1,
+   "width": "150"
+  },
+  {
+   "fieldname": "column_break_kkxv",
+   "fieldtype": "Column Break"
+  }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2023-08-01 11:09:52.584482",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Activity",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "email": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1
+  },
+  {
+   "email": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts User",
+   "share": 1
+  },
+  {
+   "email": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Quality Manager",
+   "share": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.py b/erpnext/assets/doctype/asset_activity/asset_activity.py
new file mode 100644
index 0000000..28e1b3e
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/asset_activity.py
@@ -0,0 +1,20 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+
+
+class AssetActivity(Document):
+	pass
+
+
+def add_asset_activity(asset, subject):
+	frappe.get_doc(
+		{
+			"doctype": "Asset Activity",
+			"asset": asset,
+			"subject": subject,
+			"user": frappe.session.user,
+		}
+	).insert(ignore_permissions=True, ignore_links=True)
diff --git a/erpnext/assets/doctype/asset_activity/test_asset_activity.py b/erpnext/assets/doctype/asset_activity/test_asset_activity.py
new file mode 100644
index 0000000..7a21559
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/test_asset_activity.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestAssetActivity(FrappeTestCase):
+	pass
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index a883bec..858c1db 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -18,6 +18,7 @@
 	reset_depreciation_schedule,
 	reverse_depreciation_entry_made_after_disposal,
 )
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
 from erpnext.controllers.stock_controller import StockController
 from erpnext.setup.doctype.brand.brand import get_brand_defaults
@@ -519,6 +520,13 @@
 			"fixed_asset_account", item=self.target_item_code, company=asset_doc.company
 		)
 
+		add_asset_activity(
+			asset_doc.name,
+			_("Asset created after Asset Capitalization {0} was submitted").format(
+				get_link_to_form("Asset Capitalization", self.name)
+			),
+		)
+
 		frappe.msgprint(
 			_(
 				"Asset {0} has been created. Please set the depreciation details if any and submit it."
@@ -542,9 +550,30 @@
 
 	def set_consumed_asset_status(self, asset):
 		if self.docstatus == 1:
-			asset.set_status("Capitalized" if self.target_is_fixed_asset else "Decapitalized")
+			if self.target_is_fixed_asset:
+				asset.set_status("Capitalized")
+				add_asset_activity(
+					asset.name,
+					_("Asset capitalized after Asset Capitalization {0} was submitted").format(
+						get_link_to_form("Asset Capitalization", self.name)
+					),
+				)
+			else:
+				asset.set_status("Decapitalized")
+				add_asset_activity(
+					asset.name,
+					_("Asset decapitalized after Asset Capitalization {0} was submitted").format(
+						get_link_to_form("Asset Capitalization", self.name)
+					),
+				)
 		else:
 			asset.set_status()
+			add_asset_activity(
+				asset.name,
+				_("Asset restored after Asset Capitalization {0} was cancelled").format(
+					get_link_to_form("Asset Capitalization", self.name)
+				),
+			)
 
 
 @frappe.whitelist()
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py
index b85f719..620aad8 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.py
@@ -5,6 +5,9 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
+from frappe.utils import get_link_to_form
+
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 
 
 class AssetMovement(Document):
@@ -128,5 +131,24 @@
 				current_location = latest_movement_entry[0][0]
 				current_employee = latest_movement_entry[0][1]
 
-			frappe.db.set_value("Asset", d.asset, "location", current_location)
-			frappe.db.set_value("Asset", d.asset, "custodian", current_employee)
+			frappe.db.set_value("Asset", d.asset, "location", current_location, update_modified=False)
+			frappe.db.set_value("Asset", d.asset, "custodian", current_employee, update_modified=False)
+
+			if current_location and current_employee:
+				add_asset_activity(
+					d.asset,
+					_("Asset received at Location {0} and issued to Employee {1}").format(
+						get_link_to_form("Location", current_location),
+						get_link_to_form("Employee", current_employee),
+					),
+				)
+			elif current_location:
+				add_asset_activity(
+					d.asset,
+					_("Asset transferred to Location {0}").format(get_link_to_form("Location", current_location)),
+				)
+			elif current_employee:
+				add_asset_activity(
+					d.asset,
+					_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)),
+				)
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index f649e51..7e95cb2 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -8,6 +8,7 @@
 import erpnext
 from erpnext.accounts.general_ledger import make_gl_entries
 from erpnext.assets.doctype.asset.asset import get_asset_account
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
 	get_depr_schedule,
 	make_new_active_asset_depr_schedules_and_cancel_current_ones,
@@ -25,8 +26,14 @@
 		self.calculate_total_repair_cost()
 
 	def update_status(self):
-		if self.repair_status == "Pending":
+		if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order":
 			frappe.db.set_value("Asset", self.asset, "status", "Out of Order")
+			add_asset_activity(
+				self.asset,
+				_("Asset out of order due to Asset Repair {0}").format(
+					get_link_to_form("Asset Repair", self.name)
+				),
+			)
 		else:
 			self.asset_doc.set_status()
 
@@ -68,6 +75,13 @@
 			make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
 			self.asset_doc.save()
 
+			add_asset_activity(
+				self.asset,
+				_("Asset updated after completion of Asset Repair {0}").format(
+					get_link_to_form("Asset Repair", self.name)
+				),
+			)
+
 	def before_cancel(self):
 		self.asset_doc = frappe.get_doc("Asset", self.asset)
 
@@ -95,6 +109,13 @@
 			make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
 			self.asset_doc.save()
 
+			add_asset_activity(
+				self.asset,
+				_("Asset updated after cancellation of Asset Repair {0}").format(
+					get_link_to_form("Asset Repair", self.name)
+				),
+			)
+
 	def after_delete(self):
 		frappe.get_doc("Asset", self.asset).set_status()
 
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 8426ed4..a1f0473 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -12,6 +12,7 @@
 )
 from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
 from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
 	get_asset_depr_schedule_doc,
 	get_depreciation_amount,
@@ -27,9 +28,21 @@
 	def on_submit(self):
 		self.make_depreciation_entry()
 		self.reschedule_depreciations(self.new_asset_value)
+		add_asset_activity(
+			self.asset,
+			_("Asset's value adjusted after submission of Asset Value Adjustment {0}").format(
+				get_link_to_form("Asset Value Adjustment", self.name)
+			),
+		)
 
 	def on_cancel(self):
 		self.reschedule_depreciations(self.current_asset_value)
+		add_asset_activity(
+			self.asset,
+			_("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format(
+				get_link_to_form("Asset Value Adjustment", self.name)
+			),
+		)
 
 	def validate_date(self):
 		asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
@@ -74,12 +87,16 @@
 			"account": accumulated_depreciation_account,
 			"credit_in_account_currency": self.difference_amount,
 			"cost_center": depreciation_cost_center or self.cost_center,
+			"reference_type": "Asset",
+			"reference_name": self.asset,
 		}
 
 		debit_entry = {
 			"account": depreciation_expense_account,
 			"debit_in_account_currency": self.difference_amount,
 			"cost_center": depreciation_cost_center or self.cost_center,
+			"reference_type": "Asset",
+			"reference_name": self.asset,
 		}
 
 		accounting_dimensions = get_checks_for_pl_and_bs_accounts()
diff --git a/erpnext/assets/report/asset_activity/__init__.py b/erpnext/assets/report/asset_activity/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/assets/report/asset_activity/__init__.py
diff --git a/erpnext/assets/report/asset_activity/asset_activity.json b/erpnext/assets/report/asset_activity/asset_activity.json
new file mode 100644
index 0000000..cc46775
--- /dev/null
+++ b/erpnext/assets/report/asset_activity/asset_activity.json
@@ -0,0 +1,33 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2023-08-01 11:14:46.581234",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "letterhead": null,
+ "modified": "2023-08-01 11:14:46.581234",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Activity",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Asset Activity",
+ "report_name": "Asset Activity",
+ "report_type": "Report Builder",
+ "roles": [
+  {
+   "role": "System Manager"
+  },
+  {
+   "role": "Accounts User"
+  },
+  {
+   "role": "Quality Manager"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json
index d810eff..c6b321e 100644
--- a/erpnext/assets/workspace/assets/assets.json
+++ b/erpnext/assets/workspace/assets/assets.json
@@ -183,6 +183,17 @@
    "link_type": "Report",
    "onboard": 0,
    "type": "Link"
+  },
+  {
+   "dependencies": "Asset Activity",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Asset Activity",
+   "link_count": 0,
+   "link_to": "Asset Activity",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
   }
  ],
  "modified": "2023-05-24 14:47:20.243146",