From 2ab3d752742d818ab5b6750591ed8d75cf62dec7 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 1 Aug 2023 12:00:24 +0530 Subject: [PATCH] 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 --- .../doctype/sales_invoice/sales_invoice.py | 5 +- erpnext/assets/doctype/asset/asset.json | 7 +- erpnext/assets/doctype/asset/asset.py | 33 +++++- erpnext/assets/doctype/asset/depreciation.py | 5 + .../assets/doctype/asset_activity/__init__.py | 0 .../doctype/asset_activity/asset_activity.js | 8 ++ .../asset_activity/asset_activity.json | 109 ++++++++++++++++++ .../doctype/asset_activity/asset_activity.py | 20 ++++ .../asset_activity/test_asset_activity.py | 9 ++ .../asset_capitalization.py | 31 ++++- .../doctype/asset_movement/asset_movement.py | 26 ++++- .../doctype/asset_repair/asset_repair.py | 23 +++- .../asset_value_adjustment.py | 17 +++ .../assets/report/asset_activity/__init__.py | 0 .../report/asset_activity/asset_activity.json | 33 ++++++ erpnext/assets/workspace/assets/assets.json | 11 ++ 16 files changed, 329 insertions(+), 8 deletions(-) create mode 100644 erpnext/assets/doctype/asset_activity/__init__.py create mode 100644 erpnext/assets/doctype/asset_activity/asset_activity.js create mode 100644 erpnext/assets/doctype/asset_activity/asset_activity.json create mode 100644 erpnext/assets/doctype/asset_activity/asset_activity.py create mode 100644 erpnext/assets/doctype/asset_activity/test_asset_activity.py create mode 100644 erpnext/assets/report/asset_activity/__init__.py create mode 100644 erpnext/assets/report/asset_activity/asset_activity.json diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f5ee2285d2..b0cc8ca29f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -32,6 +32,7 @@ from erpnext.assets.doctype.asset.depreciation import ( 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 @@ class SalesInvoice(SellingController): 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 @@ class SalesInvoice(SellingController): 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 2eb5f3dc3f..befb5248d5 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -533,6 +533,11 @@ "link_doctype": "Asset Depreciation Schedule", "link_fieldname": "asset" }, + { + "group": "Activity", + "link_doctype": "Asset Activity", + "link_fieldname": "asset" + }, { "group": "Journal Entry", "link_doctype": "Journal Entry", @@ -540,7 +545,7 @@ "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 9efa18ba9d..252a3dd63f 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -25,6 +25,7 @@ from erpnext.assets.doctype.asset.depreciation import ( 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 @@ class Asset(AccountsController): 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 @@ class Asset(AccountsController): "Asset Depreciation Schedules created:
{0}

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 @@ class Asset(AccountsController): 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 @@ class Asset(AccountsController): "Asset Depreciation Schedules created:
{0}

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 @@ def update_existing_asset(asset, remaining_qty, new_asset_name): }, ) + 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 @@ def create_new_asset_after_split(asset, split_qty): (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 a311bc699a..0588065d39 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -21,6 +21,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( 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 @@ def scrap_asset(asset_name): 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 @@ def restore_asset(asset_name): 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 0000000000..e69de29bb2 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 0000000000..38d3434746 --- /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 0000000000..476fb2732e --- /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 0000000000..28e1b3e32a --- /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 0000000000..7a21559c52 --- /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 a883bec71b..858c1db43c 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -18,6 +18,7 @@ from erpnext.assets.doctype.asset.depreciation import ( 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 @@ class AssetCapitalization(StockController): "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 @@ class AssetCapitalization(StockController): 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 b85f7194f9..620aad80ed 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 @@ class AssetMovement(Document): 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 f649e510f9..7e95cb2a1b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -8,6 +8,7 @@ from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_ 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 @@ class AssetRepair(AccountsController): 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 @@ class AssetRepair(AccountsController): 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 @@ class AssetRepair(AccountsController): 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 8426ed43ff..a1f047352c 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.accounts.doctype.accounting_dimension.accounting_dimension import ( ) 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 @@ class AssetValueAdjustment(Document): 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 @@ class AssetValueAdjustment(Document): "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 0000000000..e69de29bb2 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 0000000000..cc46775197 --- /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 d810effda0..c6b321e9c1 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",