diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 022b33cac10e..9ca27a734d81 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -446,3 +446,4 @@ erpnext.patches.v16_0.access_control_for_project_users erpnext.patches.v16_0.rename_ar_ap_ageing_filter erpnext.patches.v15_0.fix_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 a579fb703da1..09a5e64acd41 100644 --- a/erpnext/setup/doctype/item_group/test_item_group.py +++ b/erpnext/setup/doctype/item_group/test_item_group.py @@ -16,6 +16,8 @@ test_records = frappe.get_test_records("Item Group") +TRANSLATED_ROOT = "Todos os Grupos de Itens" + class TestItem(unittest.TestCase): def test_basic_tree(self, records=None): @@ -234,3 +236,46 @@ def test_merge_group_into_leaf(self): "_Test Item Group B - 3", merge=True, ) + + 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(self.get_root_names(), [TRANSLATED_ROOT]) + self.assertEqual( + frappe.db.get_value("Item Group", "_Test Item Group B", "parent_item_group"), TRANSLATED_ROOT + ) + self.test_basic_tree() + + # restore the original root name for the tests that follow + frappe.rename_doc("Item Group", TRANSLATED_ROOT, "All Item Groups") + self.assertEqual(self.get_root_names(), ["All Item Groups"]) + 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, + "parent_item_group": "All Item Groups", + } + ).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", "parent_item_group") + + def get_root_names(self): + return frappe.db.sql_list( + """select name from `tabItem Group` where ifnull(parent_item_group, '')=''""" + ) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 0f3356ffa50e..6197873c6fb2 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -13,6 +13,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 install(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 {