Merge pull request #57616 from mihir-kandoi/fix/item-group-root-seeding

fix: seed standard Item Groups under the existing tree root
This commit is contained in:
Mihir Kandoi
2026-07-30 13:24:52 +05:30
committed by GitHub
4 changed files with 85 additions and 6 deletions

View File

@@ -506,3 +506,4 @@ erpnext.patches.v16_0.rename_ar_ap_ageing_filter
erpnext.patches.v16_0.fix_subcontracting_titles
erpnext.patches.v16_0.move_warehouse_defaults_to_company
erpnext.patches.v16_0.backfill_repost_accounting_ledger_status
erpnext.patches.v16_0.merge_seeded_item_group_root

View File

@@ -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)

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from unittest.mock import patch
import frappe
from frappe.query_builder.functions import Max
from frappe.utils.nestedset import (
@@ -14,6 +16,8 @@ from frappe.utils.nestedset import (
from erpnext.tests.utils import ERPNextTestSuite
TRANSLATED_ROOT = "Todos os Grupos de Itens"
class TestItemGroup(ERPNextTestSuite):
def setUp(self):
@@ -204,6 +208,54 @@ class TestItemGroup(ERPNextTestSuite):
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"

View File

@@ -12,6 +12,7 @@ from frappe.desk.doctype.global_search_settings.global_search_settings import (
)
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
{