From 4ffa950aa35fd09e537b0e85210f551936bafd49 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 30 Jul 2026 13:08:28 +0530 Subject: [PATCH 1/2] fix: seed standard Item Groups under the existing tree root install_fixtures always inserted "All Item Groups" as a parentless group. On a site where another app had already created the root, ItemGroup.validate re-parented it, leaving a second group-root that held the standard groups while the real root held everything else. This is reproducible with the healthcare app on a non-English site: its after_install seeds the root as _("All Item Groups"), so a pt-BR site gets "Todos os Grupos de Itens" as the root before the setup wizard runs. The split predates #57390 -- the old translated-name lookup resolved to the same root and produced an identical tree. Resolve the root once with get_root_of (falling back to the canonical English name on fresh installs) and use it for the root record's exists-guard and the standard groups' parent, matching Company.create_default_departments. Patch merges an already-seeded "All Item Groups" into the root it sits under, lifting its children and repointing every link. Closes #57581 (cherry picked from commit e7088d89812aaca79cedc66e818206a9fad712c5) --- erpnext/patches.txt | 1 + .../v16_0/merge_seeded_item_group_root.py | 23 ++++++++ .../doctype/item_group/test_item_group.py | 52 +++++++++++++++++++ .../operations/install_fixtures.py | 15 +++--- 4 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 erpnext/patches/v16_0/merge_seeded_item_group_root.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e52582220139..e56aa77abd27 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -495,3 +495,4 @@ erpnext.patches.v16_0.enable_book_stock_expense_gl_entries erpnext.patches.v16_0.rename_ar_ap_ageing_filter erpnext.patches.v16_0.fix_subcontracting_titles erpnext.patches.v16_0.backfill_repost_accounting_ledger_status +erpnext.patches.v16_0.merge_seeded_item_group_root diff --git a/erpnext/patches/v16_0/merge_seeded_item_group_root.py b/erpnext/patches/v16_0/merge_seeded_item_group_root.py new file mode 100644 index 000000000000..95683fc96f02 --- /dev/null +++ b/erpnext/patches/v16_0/merge_seeded_item_group_root.py @@ -0,0 +1,23 @@ +import frappe +from frappe.utils.nestedset import get_root_of + +SEEDED_ROOT = "All Item Groups" + + +def execute(): + """Collapse the "All Item Groups" node seeded under a pre-existing root. + + Setup seeding always inserted "All Item Groups" as a parentless group. On a + site where another app had already created the root (under a translated + name), it was re-parented instead, leaving a second group-root holding the + standard Item Groups. + """ + root = get_root_of("Item Group") + if not root or root == SEEDED_ROOT: + return + + seeded = frappe.db.get_value("Item Group", SEEDED_ROOT, ["parent_item_group", "is_group"], as_dict=True) + if not seeded or not seeded.is_group or seeded.parent_item_group != root: + return + + frappe.rename_doc("Item Group", SEEDED_ROOT, root, merge=True, show_alert=False) diff --git a/erpnext/setup/doctype/item_group/test_item_group.py b/erpnext/setup/doctype/item_group/test_item_group.py index 4724ac8c449b..b4a4a4592483 100644 --- a/erpnext/setup/doctype/item_group/test_item_group.py +++ b/erpnext/setup/doctype/item_group/test_item_group.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt import unittest +from unittest.mock import patch + import frappe from frappe.utils.nestedset import ( NestedSetChildExistsError, @@ -14,6 +16,8 @@ from erpnext.tests.utils import ERPNextTestSuite +TRANSLATED_ROOT = "Todos os Grupos de Itens" + class TestItemGroup(ERPNextTestSuite): def setUp(self): @@ -209,6 +213,54 @@ def test_merge_group_into_leaf(self): merge=True, ) + def test_preset_records_use_existing_root(self): + from erpnext.setup.setup_wizard.operations import install_fixtures + + with patch.object(install_fixtures, "get_root_of", return_value=TRANSLATED_ROOT): + records = [ + r for r in install_fixtures.get_preset_records("India") if r["doctype"] == "Item Group" + ] + + root_record, *child_records = records + self.assertEqual(root_record["item_group_name"], TRANSLATED_ROOT) + self.assertTrue(root_record["__condition"]()) + self.assertEqual({r["parent_item_group"] for r in child_records}, {TRANSLATED_ROOT}) + + with patch.object(install_fixtures, "get_root_of", return_value="All Item Groups"): + root_record = next( + r for r in install_fixtures.get_preset_records("India") if r["doctype"] == "Item Group" + ) + self.assertFalse(root_record["__condition"]()) + + def test_patch_merges_seeded_root_into_existing_root(self): + from erpnext.patches.v16_0.merge_seeded_item_group_root import execute + + self._nest_root_under(TRANSLATED_ROOT) + self.assertEqual( + frappe.db.get_value("Item Group", "All Item Groups", "parent_item_group"), TRANSLATED_ROOT + ) + + execute() + + self.assertFalse(frappe.db.exists("Item Group", "All Item Groups")) + self.assertEqual( + frappe.get_all("Item Group", filters={"parent_item_group": ("is", "not set")}, pluck="name"), + [TRANSLATED_ROOT], + ) + self.assertEqual( + frappe.db.get_value("Item Group", "_Test Item Group B", "parent_item_group"), TRANSLATED_ROOT + ) + self.test_basic_tree() + + def _nest_root_under(self, new_root): + """Recreate the tree left behind by seeding a root under a pre-existing one.""" + frappe.get_doc({"doctype": "Item Group", "item_group_name": new_root, "is_group": 1}).insert() + + ig = frappe.qb.DocType("Item Group") + frappe.qb.update(ig).set(ig.parent_item_group, "").where(ig.name == new_root).run() + frappe.qb.update(ig).set(ig.parent_item_group, new_root).where(ig.name == "All Item Groups").run() + rebuild_tree("Item Group") + def _move_it_back(self): group_b = frappe.get_doc("Item Group", "_Test Item Group B") group_b.parent_item_group = "All Item Groups" diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 806db0719635..ef4974435d98 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -12,6 +12,7 @@ ) from frappe.desk.page.setup_wizard.setup_wizard import make_records from frappe.utils import cstr, getdate +from frappe.utils.nestedset import get_root_of from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates @@ -24,46 +25,48 @@ def read_lines(filename: str) -> list[str]: def get_preset_records(country=None): + root_item_group = get_root_of("Item Group") or _("All Item Groups") records = [ # ensure at least an empty Address Template exists for this Country {"doctype": "Address Template", "country": country}, # item group { "doctype": "Item Group", - "item_group_name": _("All Item Groups"), + "item_group_name": root_item_group, "is_group": 1, "parent_item_group": "", + "__condition": lambda: not frappe.db.exists("Item Group", root_item_group), }, { "doctype": "Item Group", "item_group_name": _("Products"), "is_group": 0, - "parent_item_group": _("All Item Groups"), + "parent_item_group": root_item_group, "show_in_website": 1, }, { "doctype": "Item Group", "item_group_name": _("Raw Material"), "is_group": 0, - "parent_item_group": _("All Item Groups"), + "parent_item_group": root_item_group, }, { "doctype": "Item Group", "item_group_name": _("Services"), "is_group": 0, - "parent_item_group": _("All Item Groups"), + "parent_item_group": root_item_group, }, { "doctype": "Item Group", "item_group_name": _("Sub Assemblies"), "is_group": 0, - "parent_item_group": _("All Item Groups"), + "parent_item_group": root_item_group, }, { "doctype": "Item Group", "item_group_name": _("Consumable"), "is_group": 0, - "parent_item_group": _("All Item Groups"), + "parent_item_group": root_item_group, }, # Stock Entry Type { From d1d214ddee8460b21ae2002784c6659d38346b70 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 30 Jul 2026 19:18:07 +0530 Subject: [PATCH 2/2] chore: fix import order in item group tests --- erpnext/setup/doctype/item_group/test_item_group.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/setup/doctype/item_group/test_item_group.py b/erpnext/setup/doctype/item_group/test_item_group.py index b4a4a4592483..82cb5c0ee40c 100644 --- a/erpnext/setup/doctype/item_group/test_item_group.py +++ b/erpnext/setup/doctype/item_group/test_item_group.py @@ -1,7 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt import unittest - from unittest.mock import patch import frappe