From 86ba4395fb861af952d7a48e9eaf49cd30eeb365 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:42:45 +0530 Subject: [PATCH 1/3] fix(selling): don't require cancel and delete perms to remove items via Update Items --- erpnext/accounts/services/child_item_update.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/services/child_item_update.py b/erpnext/accounts/services/child_item_update.py index 99b6b186116..a992aa3be69 100644 --- a/erpnext/accounts/services/child_item_update.py +++ b/erpnext/accounts/services/child_item_update.py @@ -432,6 +432,7 @@ def validate_and_delete_children(parent, data, ordered_item=None) -> bool: for d in deleted_children: validate_child_on_delete(d, parent, ordered_item) + d.flags.ignore_permissions = True d.cancel() d.delete() From 5b1c4d22e02ff3d7515d4943356e53217d411119 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:42:56 +0530 Subject: [PATCH 2/3] test(selling): cover item removal without cancel and delete perms --- .../doctype/sales_order/test_sales_order.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index ec8de1f0d90..5d16a4023fb 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -691,6 +691,44 @@ class TestSalesOrder(ERPNextTestSuite): frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name ) + def test_update_child_removing_item_without_cancel_and_delete_perms(self): + role = "_Test Sales Order Item Editor" + if not frappe.db.exists("Role", role): + frappe.get_doc({"doctype": "Role", "role_name": role, "desk_access": 1}).insert() + + frappe.permissions.add_permission("Sales Order", role, 0) + for right, value in { + "read": 1, + "write": 1, + "create": 1, + "submit": 1, + "cancel": 0, + "delete": 0, + }.items(): + frappe.permissions.update_permission_property("Sales Order", role, 0, right, value) + frappe.clear_cache() + + so = make_sales_order(**{"item_list": [{"item_code": "_Test Item", "qty": 5, "rate": 1000}]}) + trans_item = json.dumps( + [ + {"item_code": "_Test Item", "qty": 5, "rate": 1000, "docname": so.items[0].name}, + {"item_code": "_Test Item 2", "qty": 2, "rate": 500}, + ] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) + so.reload() + self.assertEqual(len(so.items), 2) + + test_user = create_user("test_so_item_editor@example.com", role, "Accounts User", "Stock User") + trans_item = json.dumps( + [{"item_code": "_Test Item", "qty": 5, "rate": 1000, "docname": so.items[0].name}] + ) + with self.set_user(test_user.name): + update_child_qty_rate("Sales Order", trans_item, so.name) + + so.reload() + self.assertEqual(len(so.items), 1) + def test_update_child_qty_rate_with_workflow(self): from frappe.model.workflow import apply_workflow From 87a403ae011c748998663f56dc6841c4b8e2e07b Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:19:52 +0530 Subject: [PATCH 3/3] test(selling): deactivate leaked sales order workflow before item removal test --- erpnext/selling/doctype/sales_order/test_sales_order.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 5d16a4023fb..7704713cb40 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -692,6 +692,13 @@ class TestSalesOrder(ERPNextTestSuite): ) def test_update_child_removing_item_without_cancel_and_delete_perms(self): + for workflow_name in frappe.get_all( + "Workflow", filters={"document_type": "Sales Order", "is_active": 1}, pluck="name" + ): + workflow = frappe.get_doc("Workflow", workflow_name) + workflow.is_active = 0 + workflow.save() + role = "_Test Sales Order Item Editor" if not frappe.db.exists("Role", role): frappe.get_doc({"doctype": "Role", "role_name": role, "desk_access": 1}).insert()