diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 0248fab4602..dd7e1d29e71 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -52,6 +52,47 @@ frappe.treeview_settings["Account"] = { ], root_label: "Accounts", get_tree_nodes: "erpnext.accounts.utils.get_children", + get_label: function (node) { + // clean display name — the account number renders as a badge (see + // onrender) instead of being glued into the name + return frappe.utils.escape_html(node.data.account_name || node.title || node.label); + }, + onrender: function (node) { + if (node.is_root || !node.data) return; + + const flags = []; + if (node.data.account_number) { + flags.push(frappe.ui.badge({ label: node.data.account_number, size: "sm" })); + } + + const company = frappe.treeview_settings["Account"].treeview?.page?.fields_dict?.company?.get_value(); + const company_currency = company && erpnext.get_currency(company); + if ( + node.data.account_currency && + company_currency && + node.data.account_currency !== company_currency + ) { + flags.push(frappe.ui.badge({ label: node.data.account_currency, theme: "blue", size: "sm" })); + } + + if (node.data.freeze_account === "Yes") { + flags.push( + $( + ` + ${frappe.utils.icon("lock", "sm")} + ` + )[0] + ); + } + + if (flags.length) { + const $flags = $( + '' + ); + flags.forEach((flag) => $flags.append(flag)); + $flags.insertAfter(node.$tree_link.find("a.tree-label")); + } + }, on_node_render: function (node, deep) { const render_balances = () => { for (let account of cur_tree.account_balance_data) { @@ -232,7 +273,7 @@ frappe.treeview_settings["Account"] = { frappe.treeview_settings["Account"].treeview["tree"] = treeview.tree; if (treeview.can_create) { treeview.page.set_primary_action( - __("New"), + { label: __("Add Account"), short_label: __("Add") }, function () { let root_company = treeview.page.fields_dict.root_company.get_value(); if (root_company) { @@ -243,13 +284,14 @@ frappe.treeview_settings["Account"] = { treeview.new_node(); } }, - "add" + "plus" ); } }, toolbar: [ { label: __("Add Child"), + icon: "plus", condition: function (node) { return ( frappe.boot.user.can_create.indexOf("Account") !== -1 && @@ -272,6 +314,7 @@ frappe.treeview_settings["Account"] = { return !node.root && frappe.boot.user.can_read.indexOf("GL Entry") !== -1; }, label: __("View Ledger"), + icon: "book-open", click: function (node, btn) { frappe.route_options = { from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], @@ -286,6 +329,106 @@ frappe.treeview_settings["Account"] = { }, btnClass: "hidden-xs", }, + { + // same label and mechanism as the Account form's Actions button: + // NOT frappe's generic rename (Allow Rename stays off) — this is + // ERPNext's controlled update that rebuilds the derived + // "number - name - abbr" document name + label: __("Update Account Name / Number"), + icon: "text-cursor-input", + condition: function (node) { + return !node.is_root && frappe.model.can_write("Account"); + }, + click: function (node) { + const dialog = new frappe.ui.Dialog({ + title: __("Update Account Number / Name"), + fields: [ + { + fieldtype: "Data", + fieldname: "account_name", + label: __("Account Name"), + reqd: 1, + default: node.data.account_name, + }, + { + fieldtype: "Data", + fieldname: "account_number", + label: __("Account Number"), + default: node.data.account_number, + }, + ], + primary_action_label: __("Update"), + primary_action(values) { + dialog.hide(); + frappe.dom.freeze(__("Updating {0}", [node.label])); + frappe.call({ + method: "erpnext.accounts.doctype.account.account.update_account_number", + args: { + name: node.label, + account_name: values.account_name, + account_number: values.account_number, + }, + callback: function (r) { + if (r.exc) return; + const treeview = frappe.views.trees["Account"]; + node.parent_node && treeview.tree.load_children(node.parent_node); + }, + always: function () { + frappe.dom.unfreeze(); + }, + }); + }, + }); + dialog.show(); + }, + }, + { + label: __("Convert to Group"), + icon: "folder-tree", + condition: function (node) { + return !node.is_root && !node.expandable && frappe.model.can_write("Account"); + }, + click: function (node) { + erpnext.accounts.convert_tree_node("Account", node, "convert_ledger_to_group"); + }, + }, + { + label: __("Convert to Non-Group"), + icon: "file-text", + condition: function (node) { + // only on groups the user has opened and found empty — a + // group with children can't convert, so don't offer it + return ( + !node.is_root && + node.expandable && + node.loaded && + !node.$ul.children().length && + frappe.model.can_write("Account") + ); + }, + click: function (node) { + erpnext.accounts.convert_tree_node("Account", node, "convert_group_to_ledger"); + }, + }, ], extend_toolbar: true, }; + +frappe.provide("erpnext.accounts"); +// shared by the Account and Cost Center tree views (defined in both files, +// whichever loads first wins): run the doctype's whitelisted convert method, +// then re-render the branch so the node's group/leaf state updates +erpnext.accounts.convert_tree_node = + erpnext.accounts.convert_tree_node || + function (doctype, node, method) { + frappe.call({ + method: "run_doc_method", + args: { dt: doctype, dn: node.label, method: method }, + callback: function (r) { + if (r.exc) return; + const treeview = frappe.views.trees[doctype]; + node.parent_node && treeview.tree.load_children(node.parent_node); + frappe.show_alert({ message: __("{0} converted", [node.label]), indicator: "green" }); + }, + }); + }; diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index b7200883124..9dac8500166 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -16,6 +16,8 @@ frappe.ui.form.on("Chart of Accounts Importer", { () => generate_tree_preview(frm), () => create_import_button(frm), () => frm.set_df_property("chart_preview", "hidden", 0), + // the preview is the point of this page — open it right away + () => frm.fields_dict.chart_preview.collapse(false), ]); } @@ -128,7 +130,6 @@ var create_import_button = function (frm) { freeze_message: __("Creating Accounts..."), callback: function (r) { if (!r.exc) { - clearInterval(frm.page["interval"]); frm.page.set_indicator(__("Import Successful"), "blue"); create_reset_button(frm); } @@ -142,42 +143,95 @@ var create_reset_button = function (frm) { frm.page .set_primary_action(__("Reset"), function () { frm.page.clear_primary_action(); - delete frm.page["show_import_button"]; frm.reload_doc(); }) .addClass("btn btn-primary"); }; -var validate_coa = function (frm) { - if (frm.doc.import_file) { - let parent = __("All Accounts"); - return frappe.call({ - method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa", - args: { - file_name: frm.doc.import_file, - parent: parent, - doctype: "Chart of Accounts Importer", - file_type: frm.doc.file_type, - for_validate: 1, - }, - callback: function (r) { - if (r.message["show_import_button"]) { - frm.page["show_import_button"] = Boolean(r.message["show_import_button"]); - } - }, - }); - } -}; - var generate_tree_preview = function (frm) { let parent = __("All Accounts"); - $(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper to load new data + const wrapper = $(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper to load new data + + // search + expand/collapse-all lean on frappe.ui.Tree helpers added with + // row mode; when running against an older frappe that predates them, skip + // this toolbar so the preview still renders (just without the extras) + const has_row_helpers = + typeof frappe.ui.Tree.prototype.get_expansion_state === "function" && + typeof frappe.ui.Tree.prototype.filter_nodes === "function"; + + let tree; + let deep_loaded = false; + let search_text = ""; + let update_buttons = () => {}; + + if (has_row_helpers) { + // same toolbar anatomy as the tree view: search on the left, + // expand/collapse-all on the right (three-state: fully collapsed -> + // Expand All, fully expanded -> Collapse All, partially expanded -> both) + const $toolbar = $('
').appendTo(wrapper); + + const search_control = frappe.ui.form.make_control({ + df: { fieldtype: "Data", fieldname: "preview_search", placeholder: __("Search") }, + parent: $toolbar, + only_input: true, + }); + search_control.refresh(); + $(search_control.wrapper).addClass("m-0").css("width", "220px"); + search_control.$input.addClass("input-xs"); + search_control.$input.on( + "input", + frappe.utils.debounce(() => { + search_text = search_control.$input.val(); + const run = () => { + // a newer keystroke superseded this one while the deep load ran + if (search_text !== search_control.$input.val()) return; + tree.filter_nodes(search_text); + }; + if (!search_text || deep_loaded) { + run(); + return; + } + tree.load_children(tree.root_node, true).then(() => { + deep_loaded = true; + run(); + }); + }, 300) + ); + + const $actions = $('').appendTo($toolbar); + update_buttons = () => { + const state = tree.get_expansion_state(); + $expand_all.prop("disabled", !(state === "collapsed" || state === "partial")); + $collapse_all.prop("disabled", !(state === "expanded" || state === "partial")); + }; + // tooltip on a wrapper: a disabled es-button has pointer-events:none, + // so hover falls through to the wrapper and the tooltip still shows + const make_action = (icon, label, onclick) => { + const $btn = $( + frappe.ui.button({ icon, disabled: true, onclick, attrs: { "aria-label": label } }) + ); + const $wrapper = $('').append($btn).appendTo($actions); + frappe.ui.tooltip($wrapper, { text: label }); + return $btn; + }; + var $expand_all = make_action("chevrons-up-down", __("Expand All"), () => { + tree.load_children(tree.root_node, true).then(() => { + deep_loaded = true; + }); + }); + var $collapse_all = make_action("chevrons-down-up", __("Collapse All"), () => { + tree.load_children(tree.root_node, false); + }); + } // generate tree structure based on the csv data - return new frappe.ui.Tree({ - parent: $(frm.fields_dict["chart_tree"].wrapper), + tree = new frappe.ui.Tree({ + parent: wrapper, label: parent, expandable: true, + // read-only preview: row-mode visuals without actions or hover cards + // (ignored by an older frappe, which renders the legacy tree) + row_style: true, method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa", args: { file_name: frm.doc.import_file, @@ -185,8 +239,9 @@ var generate_tree_preview = function (frm) { doctype: "Chart of Accounts Importer", file_type: frm.doc.file_type, }, - onclick: function (node) { - parent = node.value; - }, + on_node_render: () => update_buttons(), + // expanded flips right after this callback — check on the next tick + on_click: () => setTimeout(update_buttons, 0), }); + return tree; }; diff --git a/erpnext/accounts/doctype/cost_center/cost_center_tree.js b/erpnext/accounts/doctype/cost_center/cost_center_tree.js index 3edeb8efb0b..5b134f6545f 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center_tree.js +++ b/erpnext/accounts/doctype/cost_center/cost_center_tree.js @@ -42,6 +42,37 @@ frappe.treeview_settings["Cost Center"] = { }, ], ignore_fields: ["parent_cost_center"], + toolbar: [ + { + label: __("Convert to Group"), + icon: "folder-tree", + condition: function (node) { + return !node.is_root && !node.expandable && frappe.model.can_write("Cost Center"); + }, + click: function (node) { + erpnext.accounts.convert_tree_node("Cost Center", node, "convert_ledger_to_group"); + }, + }, + { + label: __("Convert to Non-Group"), + icon: "file-text", + condition: function (node) { + // only on groups the user has opened and found empty — a + // group with children can't convert, so don't offer it + return ( + !node.is_root && + node.expandable && + node.loaded && + !node.$ul.children().length && + frappe.model.can_write("Cost Center") + ); + }, + click: function (node) { + erpnext.accounts.convert_tree_node("Cost Center", node, "convert_group_to_ledger"); + }, + }, + ], + extend_toolbar: true, onload: function (treeview) { function get_company() { return treeview.page.fields_dict.company.get_value(); @@ -82,3 +113,22 @@ frappe.treeview_settings["Cost Center"] = { ); }, }; + +frappe.provide("erpnext.accounts"); +// shared by the Account and Cost Center tree views (defined in both files, +// whichever loads first wins): run the doctype's whitelisted convert method, +// then re-render the branch so the node's group/leaf state updates +erpnext.accounts.convert_tree_node = + erpnext.accounts.convert_tree_node || + function (doctype, node, method) { + frappe.call({ + method: "run_doc_method", + args: { dt: doctype, dn: node.label, method: method }, + callback: function (r) { + if (r.exc) return; + const treeview = frappe.views.trees[doctype]; + node.parent_node && treeview.tree.load_children(node.parent_node); + frappe.show_alert({ message: __("{0} converted", [node.label]), indicator: "green" }); + }, + }); + }; diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_template.js b/erpnext/accounts/doctype/financial_report_template/financial_report_template.js index fe04d11b2c4..321b7d2d05d 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_template.js +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.js @@ -236,6 +236,8 @@ async function refresh_tree_view(dialog, account_rows) { parent: wrapper, label: company, root_value: company, + // read-only preview: row-mode visuals without actions + row_style: true, method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_children_accounts", args: { doctype: "Account", company: company, filtered_accounts: filtered_accounts, missed: missed }, toolbar: [], diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index c2ce8ac39f9..db7a58c08c3 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1367,12 +1367,13 @@ def get_children( else: filters.append([parent_fieldname, "=", parent]) + account_fields = ["account_name", "account_number", "account_currency", "freeze_account"] if is_root: - fields += ["root_type", "report_type", "account_currency"] if doctype == "Account" else [] + fields += ["root_type", "report_type", *account_fields] if doctype == "Account" else [] filters.append(["company", "=", company]) else: - fields += ["root_type", "account_currency"] if doctype == "Account" else [] + fields += ["root_type", *account_fields] if doctype == "Account" else [] fields += [parent_fieldname + " as parent"] acc = frappe.get_list(doctype, fields=fields, filters=filters) diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js index c4ecb853d7b..1fefa43ba57 100644 --- a/erpnext/manufacturing/doctype/bom/bom_tree.js +++ b/erpnext/manufacturing/doctype/bom/bom_tree.js @@ -46,9 +46,10 @@ frappe.treeview_settings["BOM"] = { me.make_tree(); }, toolbar: [ - { toggle_btn: true }, { label: __("Edit"), + icon: "pencil", + inline: true, condition: function (node) { return node.expandable; }, diff --git a/erpnext/projects/doctype/task/task_tree.js b/erpnext/projects/doctype/task/task_tree.js index fba1b309260..561233271d6 100644 --- a/erpnext/projects/doctype/task/task_tree.js +++ b/erpnext/projects/doctype/task/task_tree.js @@ -40,6 +40,7 @@ frappe.treeview_settings["Task"] = { toolbar: [ { label: __("Add Multiple"), + icon: "list-plus", condition: function (node) { return node.expandable; }, @@ -75,7 +76,6 @@ frappe.treeview_settings["Task"] = { data: dialog.get_values()["multiple_tasks"], parent: node.data.value, }, - callback: function () {}, }); }, primary_action_label: __("Create"), diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index 73cd29b2047..8f8700af3c7 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -42,6 +42,7 @@ class BOMConfigurator { doctype: "BOM Configurator", page: this.page, expandable: true, + use_row_actions: true, title: __("Configure Product Assembly"), breadcrumb: "Manufacturing", get_tree_nodes: "erpnext.manufacturing.doctype.bom_creator.bom_creator.get_children", @@ -98,13 +99,11 @@ class BOMConfigurator { amount = frappe.format(amount, { fieldtype: "Currency", currency: frm_obj.frm.doc.currency }); $(` -