mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-08 04:01:46 +00:00
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 e7088d8981)
# Conflicts:
# erpnext/setup/doctype/item_group/test_item_group.py
# erpnext/setup/setup_wizard/operations/install_fixtures.py
This commit is contained in:
@@ -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
|
||||
|
||||
23
erpnext/patches/v16_0/merge_seeded_item_group_root.py
Normal file
23
erpnext/patches/v16_0/merge_seeded_item_group_root.py
Normal 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)
|
||||
@@ -1,8 +1,12 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
<<<<<<< HEAD
|
||||
|
||||
import unittest
|
||||
=======
|
||||
from unittest.mock import patch
|
||||
>>>>>>> e7088d8981 (fix: seed standard Item Groups under the existing tree root)
|
||||
|
||||
import frappe
|
||||
from frappe.utils.nestedset import (
|
||||
@@ -16,6 +20,8 @@ from frappe.utils.nestedset import (
|
||||
|
||||
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 +240,73 @@ class TestItem(unittest.TestCase):
|
||||
"_Test Item Group B - 3",
|
||||
merge=True,
|
||||
)
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
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"
|
||||
group_b.save()
|
||||
self.test_basic_tree()
|
||||
|
||||
def _get_no_of_children(self, item_group):
|
||||
def get_no_of_children(item_groups, no_of_children):
|
||||
children = []
|
||||
for ig in item_groups:
|
||||
children += frappe.get_all("Item Group", filters={"parent_item_group": ig}, pluck="name")
|
||||
|
||||
if len(children):
|
||||
return get_no_of_children(children, no_of_children + len(children))
|
||||
else:
|
||||
return no_of_children
|
||||
|
||||
return get_no_of_children([item_group], 0)
|
||||
>>>>>>> e7088d8981 (fix: seed standard Item Groups under the existing tree root)
|
||||
|
||||
@@ -13,6 +13,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
|
||||
@@ -23,47 +24,53 @@ def read_lines(filename: str) -> list[str]:
|
||||
return (Path(__file__).parent.parent / "data" / filename).read_text().splitlines()
|
||||
|
||||
|
||||
<<<<<<< HEAD
|
||||
def install(country=None):
|
||||
=======
|
||||
def get_preset_records(country=None):
|
||||
root_item_group = get_root_of("Item Group") or _("All Item Groups")
|
||||
>>>>>>> e7088d8981 (fix: seed standard Item Groups under the existing tree root)
|
||||
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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user