From 2edd12b26d8d226e32d6ac9c3ae14ff61d90e3f5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:51:18 +0530 Subject: [PATCH] fix: prevent cancellation of last asset movement (backport #47291) (#47312) fix: prevent cancellation of last asset movement (#47291) * fix: prevent cancellation of last asset movement * test: movement cancellation * fix: allow cancellation of asset movement when cancelling asset (cherry picked from commit 9dee4ac891cac2b9121c3d459554b65c52cb4198) Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> --- .../doctype/asset_movement/asset_movement.py | 12 ++++++ .../asset_movement/test_asset_movement.py | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index ad9b4380fa9..c68f9044ae1 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -152,6 +152,9 @@ class AssetMovement(Document): """, args, ) + + self.validate_movement_cancellation(d, latest_movement_entry) + if latest_movement_entry: current_location = latest_movement_entry[0][0] current_employee = latest_movement_entry[0][1] @@ -179,3 +182,12 @@ class AssetMovement(Document): d.asset, _("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)), ) + + def validate_movement_cancellation(self, row, latest_movement_entry): + asset_doc = frappe.get_doc("Asset", row.asset) + if not latest_movement_entry and asset_doc.docstatus == 1: + frappe.throw( + _( + "Asset {0} has only one movement record. Please create another movement before deleting this one to maintain asset tracking." + ).format(row.asset) + ) diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 52590d2ba86..2d0d68cb25f 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -147,6 +147,45 @@ class TestAssetMovement(unittest.TestCase): movement1.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") + def test_last_movement_cancellation_validation(self): + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") + asset = frappe.get_doc("Asset", asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = "2020-06-06" + asset.purchase_date = "2020-06-06" + asset.append( + "finance_books", + { + "expected_value_after_useful_life": 10000, + "next_depreciation_date": "2020-12-31", + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + }, + ) + if asset.docstatus == 0: + asset.submit() + + AssetMovement = frappe.qb.DocType("Asset Movement") + AssetMovementItem = frappe.qb.DocType("Asset Movement Item") + + asset_movement = ( + frappe.qb.from_(AssetMovement) + .join(AssetMovementItem) + .on(AssetMovementItem.parent == AssetMovement.name) + .select(AssetMovement.name) + .where( + (AssetMovementItem.asset == asset.name) + & (AssetMovement.company == asset.company) + & (AssetMovement.docstatus == 1) + ) + ).run(as_dict=True) + + asset_movement_doc = frappe.get_doc("Asset Movement", asset_movement[0].name) + self.assertRaises(frappe.ValidationError, asset_movement_doc.cancel) + def create_asset_movement(**args): args = frappe._dict(args)