chore: some more fixtures

This commit is contained in:
sokumon
2026-08-28 01:27:52 +05:30
parent 2a14b5d953
commit 987408f203
5 changed files with 50 additions and 228 deletions

View File

@@ -864,7 +864,8 @@
"open_in_new_tab": 0,
"route_options": "{\"is_return\": 1}",
"show_arrow": 0,
"type": "Link"
"type": "Link",
"filters": "{\"is_return\": 1}"
},
{
"added": 0,
@@ -940,7 +941,8 @@
"open_in_new_tab": 0,
"route_options": "{\"is_return\": 1}",
"show_arrow": 0,
"type": "Link"
"type": "Link",
"filters": "{\"is_return\": 1}"
},
{
"added": 0,
@@ -1152,22 +1154,6 @@
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"icon": "settings",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Settings",
"link_to": "Accounts Settings",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
@@ -1891,7 +1877,7 @@
"type": "Link"
}
],
"modified": "2026-08-26 16:36:51.109018",
"modified": "2026-08-28 12:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts",

View File

@@ -9,151 +9,172 @@
"added": 0,
"hidden": 0,
"icon": "landmark",
"sidebar": "Accounts",
"link_to": "Accounts",
"link_type": "Sidebar",
"title": "Accounts"
},
{
"added": 0,
"hidden": 0,
"icon": "handshake",
"sidebar": "CRM",
"link_to": "CRM",
"link_type": "Sidebar",
"title": "CRM"
},
{
"added": 0,
"hidden": 0,
"icon": "shopping-cart",
"sidebar": "Buying",
"link_to": "Buying",
"link_type": "Sidebar",
"title": "Buying"
},
{
"added": 0,
"hidden": 0,
"icon": "folder-kanban",
"sidebar": "Projects",
"link_to": "Projects",
"link_type": "Sidebar",
"title": "Projects"
},
{
"added": 0,
"hidden": 0,
"icon": "store",
"sidebar": "Selling",
"link_to": "Selling",
"link_type": "Sidebar",
"title": "Selling"
},
{
"added": 0,
"hidden": 0,
"icon": "sliders-horizontal",
"sidebar": "Setup",
"link_to": "Setup",
"link_type": "Sidebar",
"title": "Setup"
},
{
"added": 0,
"hidden": 0,
"icon": "building-2",
"sidebar": "Manufacturing",
"link_to": "Manufacturing",
"link_type": "Sidebar",
"title": "Manufacturing"
},
{
"added": 0,
"hidden": 0,
"icon": "package",
"sidebar": "Stock",
"link_to": "Stock",
"link_type": "Sidebar",
"title": "Stock"
},
{
"added": 0,
"hidden": 0,
"icon": "headset",
"sidebar": "Support",
"link_to": "Support",
"link_type": "Sidebar",
"title": "Support"
},
{
"added": 0,
"hidden": 0,
"icon": "pocket-knife",
"sidebar": "Utilities",
"link_to": "Utilities",
"link_type": "Sidebar",
"title": "Utilities"
},
{
"added": 0,
"hidden": 0,
"icon": "archive",
"sidebar": "Assets",
"link_to": "Assets",
"link_type": "Sidebar",
"title": "Assets"
},
{
"added": 0,
"hidden": 0,
"icon": "panels-top-left",
"sidebar": "Portal",
"link_to": "Portal",
"link_type": "Sidebar",
"title": "Portal"
},
{
"added": 0,
"hidden": 0,
"icon": "wrench",
"sidebar": "Maintenance",
"link_to": "Maintenance",
"link_type": "Sidebar",
"title": "Maintenance"
},
{
"added": 0,
"hidden": 0,
"icon": "globe",
"sidebar": "Regional",
"link_to": "Regional",
"link_type": "Sidebar",
"title": "Regional"
},
{
"added": 0,
"hidden": 0,
"icon": "plug",
"sidebar": "ERPNext Integrations",
"link_to": "ERPNext Integrations",
"link_type": "Sidebar",
"title": "Integrations"
},
{
"added": 0,
"hidden": 0,
"icon": "shield-check",
"sidebar": "Quality Management",
"link_to": "Quality",
"link_type": "Sidebar",
"title": "Quality"
},
{
"added": 0,
"hidden": 0,
"icon": "messages-square",
"sidebar": "Communication",
"link_to": "Communication",
"link_type": "Sidebar",
"title": "Communication"
},
{
"added": 0,
"hidden": 0,
"icon": "phone",
"sidebar": "Telephony",
"link_to": "Telephony",
"link_type": "Sidebar",
"title": "Telephony"
},
{
"added": 0,
"hidden": 0,
"icon": "layers",
"sidebar": "Bulk Transaction",
"link_to": "Bulk Transaction",
"link_type": "Sidebar",
"title": "Bulk Transaction"
},
{
"added": 0,
"hidden": 0,
"icon": "factory",
"sidebar": "Subcontracting",
"link_to": "Subcontracting",
"link_type": "Sidebar",
"title": "Subcontracting"
},
{
"added": 0,
"hidden": 0,
"icon": "file-code",
"sidebar": "EDI",
"link_to": "EDI",
"link_type": "Sidebar",
"title": "EDI"
}
],
"modified": "2026-08-26 23:30:00.000000",
"modified": "2026-08-28 12:00:00.000000",
"modified_by": "Administrator",
"name": "erpnext",
"owner": "Administrator",

View File

@@ -195,10 +195,10 @@
"type": "Link"
}
],
"modified": "2026-08-16 00:00:00.000000",
"modified": "2026-08-28 12:00:00.000000",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Management",
"name": "Quality",
"owner": "Administrator",
"sequence_id": 0.0,
"standard": 1,

View File

@@ -1,185 +0,0 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
"""erpnext ships its navigation as `Sidebar` fixtures, one per module that has an arrangement.
The framework renamed `Module Sidebar` to `Sidebar` and moved an app's fixtures from
`<module>/module_sidebar/` to `<module>/sidebar/`. An app that has not followed is not broken --
its folder is simply never walked, and each of its modules falls back to a base computed from its
own contents -- so nothing here fails loudly if the conversion is half done. That is exactly why
it is asserted: the failure mode is erpnext's curated navigation quietly reverting to generated.
Two facts, and they are different questions:
- `TestTheFixturesAreWhereMigrateLooks` is about the *files*. Import finds them, and orphan
removal derives the same record name from the path that the file declares -- a mismatch there
makes migrate delete the very rows it just imported.
- `TestTheModulesResolveToTheirShippedArrangement` is about *navigation*, asked through the
resolver seam rather than through any payload key. It is what says the files actually took.
"""
import json
import os
import frappe
from frappe.desk.doctype.sidebar.convert_fixtures import export_path
from frappe.desk.doctype.sidebar.sidebar import resolve_sidebar
from frappe.model.sync import create_entity_file_map, get_doc_files
from erpnext.tests.utils import ERPNextTestSuite
#: Every module that ships a `Sidebar`. `Banking` is deliberately absent -- it is new, has no
#: arrangement yet, and a module with no fixture is not baseless: it is served by a computed base
#: built from its own contents.
#:
#: Eleven of these are real arrangements, and ten are not, and the difference is worth knowing
#: before reading a failure. `Accounts` (124 items), `Selling` (62), `Stock` (56) and the rest of
#: the large modules carry navigation somebody arranged. The ten small ones -- `Bulk Transaction`,
#: `Communication`, `EDI`, `ERPNext Integrations`, `Maintenance`, `Portal`, `Regional`,
#: `Subcontracting`, `Telephony`, `Utilities` -- are a computed base that was materialized and
#: frozen, legible in the rows: `icon: settings` on exactly the doctypes with "settings" in the
#: name, and the `Reports` section label as a code constant out of `generate_items`.
#:
#: For those ten the header icon is the only authored part, which makes it the assertion that
#: separates shipped from computed for every module here: `build_computed_base` hands out
#: `hammer`, and not one of these twenty-one says `hammer`.
SIDEBAR_MODULES = [
"Accounts",
"Assets",
"Bulk Transaction",
"Buying",
"CRM",
"Communication",
"EDI",
"ERPNext Integrations",
"Maintenance",
"Manufacturing",
"Portal",
"Projects",
"Quality Management",
"Regional",
"Selling",
"Setup",
"Stock",
"Subcontracting",
"Support",
"Telephony",
"Utilities",
]
#: `Portal` ships a fixture with an empty item list, so it has no arrangement to resolve to.
#: `get_sidebar_bases` fills an empty document's rows from the computed base and keeps only what
#: the document says about *itself* -- title, icon, app -- so every resolution fact below would be
#: a fact about the framework's fallback rather than about erpnext's authoring. It is named here,
#: not filtered out by a rule, so that a fixture which lost its items has to be excluded by hand.
MODULES_WITH_SHIPPED_ITEMS = [module for module in SIDEBAR_MODULES if module != "Portal"]
def shipped(module: str) -> dict:
"""The fixture as it sits in the app folder, before any site has seen it."""
with open(export_path(module)) as f:
return json.load(f)
class TestTheFixturesAreWhereMigrateLooks(ERPNextTestSuite):
def test_every_authoring_module_ships_one(self):
"""Named individually rather than globbed: a fixture that stopped being exported would
pass a test that only checks the files it can find."""
for module in SIDEBAR_MODULES:
with self.subTest(module=module):
self.assertTrue(os.path.exists(export_path(module)))
def test_they_declare_the_renamed_doctype(self):
"""A fixture still naming `Module Sidebar` would import against a doctype the site no
longer has -- which is why the walk skips the old folder rather than failing on it."""
for module in SIDEBAR_MODULES:
with self.subTest(module=module):
self.assertEqual(shipped(module)["doctype"], "Sidebar")
def test_the_module_walk_picks_them_up(self):
"""`get_doc_files` is what migrate imports from. It only opens folders named by
`IMPORTABLE_DOCTYPES`, so this is the fact that the folder rename landed."""
for module in SIDEBAR_MODULES:
with self.subTest(module=module):
module_path = frappe.get_module_path(module)
self.assertIn(export_path(module), get_doc_files(files=[], start_path=module_path))
def test_record_name_and_filename_agree(self):
"""Orphan removal maps a file to a record by reading the `name` out of it and looking for
that record. Standard rows whose file it cannot find are deleted, so a fixture whose name
and path disagree is imported and then reaped on the same migrate."""
known = create_entity_file_map(["Sidebar"])["Sidebar"]
for module in SIDEBAR_MODULES:
with self.subTest(module=module):
self.assertEqual(shipped(module)["name"], module)
self.assertEqual(known.get(module), export_path(module))
def test_they_store_no_item_key(self):
"""A base row's identity is derived from its own columns, so `Sidebar.clear_stored_keys`
nulls `key` on the way in and `no_nulls=True` drops it on the way back out. A fixture
still carrying one disagrees with what a developer-mode re-export would write, which is
how a diff nobody authored appears -- and frappe's own eleven shipped fixtures carry none.
Only `key`. A Check field valued `0` is not null, so `is_default_module` does survive an
export and every one of frappe's fixtures ships it; dropping it here would be the same
divergence in the other direction."""
for module in SIDEBAR_MODULES:
with self.subTest(module=module):
for item in shipped(module)["items"]:
self.assertNotIn("key", item)
def test_portal_ships_no_arrangement_of_its_own(self):
"""Named rather than left implicit, because it is why `Portal` is absent from every
resolution fact below. An empty document is not a hidden module: `get_sidebar_bases` fills
its rows from the computed base and keeps only what it says about itself, so what this
fixture contributes is its icon."""
self.assertEqual(shipped("Portal")["items"], [])
self.assertTrue(shipped("Portal")["header_icon"])
self.assertNotIn("Portal", MODULES_WITH_SHIPPED_ITEMS)
class TestTheModulesResolveToTheirShippedArrangement(ERPNextTestSuite):
"""The point of the whole exercise: what a person's navigation resolves to.
Asserted as Administrator, who is filtered out of nothing and carries no customization, so the
resolution is the shipped arrangement itself rather than one reader's view of it. That a
*restricted* reader sees less is the framework's fact and is asserted there.
"""
def test_a_module_resolves_to_the_items_its_fixture_ships(self):
"""Labels in order, which is the whole of what the fixture authored. A computed base would
still resolve to something -- these modules have doctypes -- so "resolves to anything at
all" is not the fact worth asserting."""
for module in MODULES_WITH_SHIPPED_ITEMS:
with self.subTest(module=module):
resolved = resolve_sidebar(module, "Administrator")
self.assertIsNotNone(resolved)
self.assertEqual(
[item["label"] for item in resolved.items],
[item["label"] for item in shipped(module)["items"]],
)
def test_the_label_and_icon_are_the_fixture_s(self):
"""`resolve_sidebar` answers `None` for a scope that resolves to nothing, and that is the
failure this file exists to catch -- so it is asserted against rather than skipped over.
Ten fixtures failing to import would otherwise pass this test in silence."""
for module in MODULES_WITH_SHIPPED_ITEMS:
with self.subTest(module=module):
resolved = resolve_sidebar(module, "Administrator")
self.assertIsNotNone(resolved)
fixture = shipped(module)
self.assertEqual(resolved.label, fixture["title"])
self.assertEqual(resolved.header_icon, fixture["header_icon"])
def test_a_module_opens_on_its_own_navigation(self):
"""Landing is derived from the resolved entries, so a module falling back to a computed
base would land somewhere the fixture never named."""
for module in MODULES_WITH_SHIPPED_ITEMS:
with self.subTest(module=module):
resolved = resolve_sidebar(module, "Administrator")
self.assertIsNotNone(resolved)
self.assertIsNotNone(resolved.landing)