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",