From e7ae29661447f1f58f43b3d44a77f186f096b727 Mon Sep 17 00:00:00 2001 From: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com> Date: Wed, 13 May 2026 14:53:04 +0530 Subject: [PATCH] fix(UX): Buying settings form cleanup (#54731) * fix(UX): Buying settings form cleanup * fix: controller approach modification * fix: dark mode support (cherry picked from commit 45f05fbeaa6ef99314e438067f18766be7993fb7) --- .../buying_settings/buying_settings.js | 53 +++++- .../buying_settings/buying_settings.json | 159 ++++++++++++------ erpnext/public/js/erpnext.bundle.js | 2 +- ...ming_series_dialog.js => naming_series.js} | 119 ++++++++++++- .../selling_settings/selling_settings.js | 65 +++---- 5 files changed, 289 insertions(+), 109 deletions(-) rename erpnext/public/js/utils/{naming_series_dialog.js => naming_series.js} (73%) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.js b/erpnext/buying/doctype/buying_settings/buying_settings.js index 11b920aea71..9cc43220226 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.js +++ b/erpnext/buying/doctype/buying_settings/buying_settings.js @@ -2,10 +2,59 @@ // For license information, please see license.txt frappe.ui.form.on("Buying Settings", { - // refresh: function(frm) { - // } + refresh(frm) { + if (!frm.naming_controller) frm.naming_controller = new erpnext.NamingSeriesController(frm); + + const display = frm.doc.supp_master_name === "Naming Series"; + frm.set_df_property("naming_series_details", "hidden", !display); + frm.set_df_property("configure", "hidden", !display); + + if (display) { + frm.naming_controller.load_master_series("Supplier", "naming_series_details"); + } + + frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm)); + }, + + supp_master_name(frm) { + const display = frm.doc.supp_master_name === "Naming Series"; + frm.set_df_property("naming_series_details", "hidden", !display); + frm.set_df_property("configure", "hidden", !display); + + if (display) { + frm.naming_controller.load_master_series("Supplier", "naming_series_details"); + } else { + frm.doc.naming_series_details = ""; + frm.refresh_field("naming_series_details"); + } + + frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm)); + }, + + configure(frm) { + frm.naming_controller.show_naming_series_dialog("Supplier", ({ naming_series_options }) => { + frm.doc.naming_series_details = naming_series_options; + frm.refresh_field("naming_series_details"); + }); + }, }); +function get_transactions(frm) { + const transactions = [ + { label: __("Supplier"), doctype: "Supplier" }, + { label: __("Material Request"), doctype: "Material Request" }, + { label: __("Request for Quotation"), doctype: "Request for Quotation" }, + { label: __("Purchase Order"), doctype: "Purchase Order" }, + { label: __("Purchase Invoice"), doctype: "Purchase Invoice" }, + { label: __("Purchase Receipt"), doctype: "Purchase Receipt" }, + ]; + + if (frm.doc.supp_master_name !== "Naming Series") { + return transactions.filter((t) => t.doctype !== "Supplier"); + } + + return transactions; +} frappe.tour["Buying Settings"] = [ { fieldname: "supp_master_name", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 3963bbb4959..6c2d2f1bb99 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -6,44 +6,51 @@ "engine": "InnoDB", "field_order": [ "supplier_and_price_defaults_section", + "supplier_defaults_section", "supp_master_name", - "supplier_group", - "buying_price_list", + "naming_series_details", + "configure", "column_break_4", + "supplier_group", + "pricing_tab", + "buying_price_list", + "section_break_vwgg", + "maintain_same_rate", + "column_break_lwxs", "maintain_same_rate_action", "role_to_override_stop_action", - "section_break_xmlt", - "po_required", - "blanket_order_allowance", - "column_break_sbwq", - "pr_required", - "project_update_frequency", "transaction_settings_section", "column_break_fcyl", - "set_landed_cost_based_on_purchase_invoice_rate", - "allow_zero_qty_in_supplier_quotation", - "use_transaction_date_exchange_rate", - "allow_zero_qty_in_request_for_quotation", - "allow_negative_rates_for_items", + "po_required", + "pr_required", + "project_update_frequency", "column_break_12", - "maintain_same_rate", "allow_multiple_items", - "bill_for_rejected_quantity_in_purchase_invoice", + "allow_negative_rates_for_items", "set_valuation_rate_for_rejected_materials", "disable_last_purchase_rate", "show_pay_button", + "purchase_invoice_settings_section", + "bill_for_rejected_quantity_in_purchase_invoice", + "use_transaction_date_exchange_rate", + "set_landed_cost_based_on_purchase_invoice_rate", + "zero_quantity_line_items_section", + "allow_zero_qty_in_supplier_quotation", + "allow_zero_qty_in_request_for_quotation", "allow_zero_qty_in_purchase_order", + "blanket_order_section", + "blanket_order_allowance", "subcontract", "backflush_raw_materials_of_subcontract_based_on", - "column_break_11", "over_transfer_allowance", "validate_consumed_qty", "section_break_xcug", "auto_create_subcontracting_order", - "column_break_izrr", "auto_create_purchase_receipt", "request_for_quotation_tab", - "fixed_email" + "fixed_email", + "document_naming_tab", + "transaction_naming_html" ], "fields": [ { @@ -54,6 +61,7 @@ "options": "Supplier Name\nNaming Series\nAuto Name" }, { + "documentation_url": "https://docs.frappe.io/erpnext/buying-settings#2-default-supplier-group", "fieldname": "supplier_group", "fieldtype": "Link", "label": "Default Supplier Group", @@ -68,26 +76,27 @@ { "fieldname": "po_required", "fieldtype": "Select", - "label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?", + "label": "Is Purchase Order required for Purchase Invoice & Receipt creation?", "options": "No\nYes" }, { "fieldname": "pr_required", "fieldtype": "Select", - "label": "Is Purchase Receipt Required for Purchase Invoice Creation?", + "label": "Is Purchase Receipt required for Purchase Invoice creation?", "options": "No\nYes" }, { "default": "0", + "description": "Warn or stop if Item rate is changed in Purchase Invoice or Purchase Receipt generated from a Purchase Order.", "fieldname": "maintain_same_rate", "fieldtype": "Check", - "label": "Maintain Same Rate Throughout the Purchase Cycle" + "label": "Maintain same rate throughout the purchase cycle" }, { "default": "0", "fieldname": "allow_multiple_items", "fieldtype": "Check", - "label": "Allow Item To Be Added Multiple Times in a Transaction" + "label": "Allow Item to be added multiple times in a transaction" }, { "fieldname": "subcontract", @@ -96,9 +105,10 @@ }, { "default": "BOM", + "documentation_url": "https://docs.frappe.io/erpnext/buying-settings#1-backflush-raw-materials-of-subcontract-based-on", "fieldname": "backflush_raw_materials_of_subcontract_based_on", "fieldtype": "Select", - "label": "Backflush Raw Materials of Subcontract Based On", + "label": "Backflush raw materials of subcontract based on", "options": "BOM\nMaterial Transferred for Subcontract" }, { @@ -108,25 +118,21 @@ "fieldtype": "Float", "label": "Over Transfer Allowance (%)" }, - { - "fieldname": "column_break_11", - "fieldtype": "Column Break" - }, { "default": "Stop", "depends_on": "maintain_same_rate", "description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.", "fieldname": "maintain_same_rate_action", "fieldtype": "Select", - "label": "Action If Same Rate is Not Maintained", + "label": "Action if same rate is not maintained", "mandatory_depends_on": "maintain_same_rate", "options": "Stop\nWarn" }, { - "depends_on": "eval:doc.maintain_same_rate_action == 'Stop'", + "depends_on": "maintain_same_rate", "fieldname": "role_to_override_stop_action", "fieldtype": "Link", - "label": "Role Allowed to Override Stop Action", + "label": "Role allowed to override stop action", "options": "Role" }, { @@ -134,12 +140,12 @@ "description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.", "fieldname": "bill_for_rejected_quantity_in_purchase_invoice", "fieldtype": "Check", - "label": "Bill for Rejected Quantity in Purchase Invoice" + "label": "Bill for rejected quantity in Purchase Invoice" }, { "fieldname": "supplier_and_price_defaults_section", "fieldtype": "Tab Break", - "label": "Naming Series and Price Defaults" + "label": "Defaults" }, { "fieldname": "column_break_4", @@ -156,16 +162,17 @@ }, { "default": "0", + "description": "Prevents the system from automatically using the rate from the last purchase transaction when creating new purchase orders or transactions.", "fieldname": "disable_last_purchase_rate", "fieldtype": "Check", - "label": "Disable Last Purchase Rate" + "label": "Disable last purchase rate" }, { "default": "1", "depends_on": "eval: frappe.boot.versions && frappe.boot.versions.payments", "fieldname": "show_pay_button", "fieldtype": "Check", - "label": "Show Pay Button in Purchase Order Portal" + "label": "Show pay button in Purchase Order portal" }, { "default": "0", @@ -193,30 +200,25 @@ "fieldname": "section_break_xcug", "fieldtype": "Section Break" }, - { - "fieldname": "column_break_izrr", - "fieldtype": "Column Break" - }, { "default": "0", "description": "Subcontracting Order (Draft) will be auto-created on submission of Purchase Order.", "fieldname": "auto_create_subcontracting_order", "fieldtype": "Check", - "label": "Auto Create Subcontracting Order" + "label": "Auto create Subcontracting Order" }, { "default": "0", "description": "Purchase Receipt (Draft) will be auto-created on submission of Subcontracting Receipt.", "fieldname": "auto_create_purchase_receipt", "fieldtype": "Check", - "label": "Auto Create Purchase Receipt" + "label": "Auto create Purchase Receipt" }, { "default": "Each Transaction", - "description": "How often should Project be updated of Total Purchase Cost ?", "fieldname": "project_update_frequency", "fieldtype": "Select", - "label": "Update frequency of Project", + "label": "How often should project be updated of Total Purchase Cost ?", "options": "Each Transaction\nManual" }, { @@ -240,14 +242,6 @@ "fieldtype": "Check", "label": "Allow Supplier Quotation with Zero Quantity" }, - { - "fieldname": "section_break_xmlt", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_sbwq", - "fieldtype": "Column Break" - }, { "fieldname": "column_break_fcyl", "fieldtype": "Column Break" @@ -258,7 +252,7 @@ "description": "If enabled, the system will generate an accounting entry for materials rejected in the Purchase Receipt.", "fieldname": "set_valuation_rate_for_rejected_materials", "fieldtype": "Check", - "label": "Set Valuation Rate for Rejected Materials" + "label": "Set valuation rate for rejected Materials" }, { "fieldname": "request_for_quotation_tab", @@ -279,23 +273,77 @@ "description": "Raw materials consumed qty will be validated based on FG BOM required qty", "fieldname": "validate_consumed_qty", "fieldtype": "Check", - "label": "Validate Consumed Qty (as per BOM)" + "label": "Validate consumed quantity (as per BOM)" }, { "default": "0", "fieldname": "allow_negative_rates_for_items", "fieldtype": "Check", - "label": "Allow Negative rates for Items" + "label": "Allow negative rates for Items" + }, + { + "fieldname": "supplier_defaults_section", + "fieldtype": "Section Break", + "label": "Supplier Defaults" + }, + { + "fieldname": "section_break_vwgg", + "fieldtype": "Section Break" + }, + { + "fieldname": "blanket_order_section", + "fieldtype": "Section Break", + "label": "Blanket Orders" + }, + { + "fieldname": "zero_quantity_line_items_section", + "fieldtype": "Section Break", + "label": "Zero-Quantity Line Items" + }, + { + "fieldname": "purchase_invoice_settings_section", + "fieldtype": "Section Break", + "label": "Purchase Invoice Settings" + }, + { + "fieldname": "column_break_lwxs", + "fieldtype": "Column Break" + }, + { + "fieldname": "pricing_tab", + "fieldtype": "Tab Break", + "label": "Pricing" + }, + { + "fieldname": "document_naming_tab", + "fieldtype": "Tab Break", + "label": "Document Naming" + }, + { + "fieldname": "configure", + "fieldtype": "Button", + "hidden": 1, + "label": "Configure Series" + }, + { + "fieldname": "transaction_naming_html", + "fieldtype": "HTML" + }, + { + "fieldname": "naming_series_details", + "fieldtype": "Small Text", + "hidden": 1, + "is_virtual": 1, + "label": "Naming Series options" } ], "grid_page_length": 50, - "hide_toolbar": 0, "icon": "fa fa-cog", "idx": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-04-15 16:07:35.484787", + "modified": "2026-05-05 16:30:37.184607", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -313,7 +361,6 @@ { "create": 1, "email": 1, - "export": 1, "print": 1, "read": 1, "role": "Purchase Manager", diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index eca43de6f83..69b587ba002 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -37,6 +37,6 @@ import "./utils/demo.js"; import "./financial_statements.js"; import "./sales_trends_filters.js"; import "./purchase_trends_filters.js"; -import "./utils/naming_series_dialog.js"; +import "./utils/naming_series.js"; // import { sum } from 'frappe/public/utils/util.js' diff --git a/erpnext/public/js/utils/naming_series_dialog.js b/erpnext/public/js/utils/naming_series.js similarity index 73% rename from erpnext/public/js/utils/naming_series_dialog.js rename to erpnext/public/js/utils/naming_series.js index b7f1ff076a0..9a1725529aa 100644 --- a/erpnext/public/js/utils/naming_series_dialog.js +++ b/erpnext/public/js/utils/naming_series.js @@ -207,6 +207,7 @@ erpnext.NamingSeriesTable = class NamingSeriesTable { this.frm = opts.frm; this.transactions = opts.transactions || []; this.$wrapper = opts.frm.get_field(opts.fieldname).$wrapper; + this.theme_observer = null; } render() { this.$wrapper.html(` @@ -231,6 +232,21 @@ erpnext.NamingSeriesTable = class NamingSeriesTable { const $rows = this.$wrapper.find(".naming-series-table-rows"); this.map_configure_button($rows); this.get_row_data($rows); + + if (this.theme_observer) { + this.theme_observer.disconnect(); + } + + const observer = new MutationObserver(() => { + const badge_class = this.get_current_badge_class(); + + this.$wrapper.find(".badge").removeClass("badge-light badge-dark").addClass(badge_class); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-theme"], + }); } map_configure_button($rows) { @@ -258,9 +274,11 @@ erpnext.NamingSeriesTable = class NamingSeriesTable { }); } - get_row_data($rows) { - this.transactions.forEach((t) => { - frappe.model.with_doctype(t.doctype, () => { + async get_row_data($rows) { + const rows = await Promise.all( + this.transactions.map(async (t) => { + await new Promise((resolve) => frappe.model.with_doctype(t.doctype, resolve)); + const meta = frappe.get_meta(t.doctype); const naming_df = (meta?.fields || []).find((df) => df.fieldname === "naming_series"); const series = (naming_df?.options || "") @@ -268,9 +286,12 @@ erpnext.NamingSeriesTable = class NamingSeriesTable { .map((s) => s.trim()) .filter(Boolean); - $rows.append(this.make_row(t, series)); - }); - }); + return this.make_row(t, series); + }) + ); + + $rows.empty(); + rows.forEach(($row) => $rows.append($row)); } make_row(t, series) { @@ -296,13 +317,20 @@ erpnext.NamingSeriesTable = class NamingSeriesTable { `); } + get_current_badge_class() { + return document.documentElement.getAttribute("data-theme") === "dark" ? "badge-dark" : "badge-light"; + } + series_list_background(series_list) { if (!series_list.length) { return `${__("Not configured")}`; } + + const badge_class = this.get_current_badge_class(); + return series_list .map( - (s) => ` ` ${frappe.utils.escape_html(s)} ` @@ -310,3 +338,80 @@ erpnext.NamingSeriesTable = class NamingSeriesTable { .join(""); } }; + +/** + * @param {Object} frm - Frappe form instance. + */ +erpnext.NamingSeriesController = class NamingSeriesController { + constructor(frm) { + this.frm = frm; + } + + /** + * Renders the naming series table in the given field. + * + * @param {string} fieldname - Fieldname where the table should be rendered. + * @param {Array<{doctype: string, label: string}>} [transactions=[]] - Transactions to display. + * @returns {void} + */ + render_table(fieldname, transactions = []) { + this.frm._naming_series_table = new erpnext.NamingSeriesTable({ + frm: this.frm, + fieldname: fieldname, + transactions: transactions, + }); + this.frm._naming_series_table.render(); + } + + /** + * Loads naming series from the given master doctype into a field. + * + * @param {string} doctype - Master doctype name. + * @param {string} field - Fieldname where series should be shown. + * @returns {void} + */ + load_master_series(doctype, field) { + frappe.model.with_doctype(doctype, () => { + const meta = frappe.get_meta(doctype); + const naming_df = (meta?.fields || []).find((df) => df.fieldname === "naming_series"); + const options = naming_df?.options || ""; + const series_list = options + .split("\n") + .map((s) => s.trim()) + .filter(Boolean); + + this.frm.doc[field] = series_list.length + ? series_list.join("\n") + : __("No naming series defined"); + this.frm.refresh_field(field); + }); + } + + /** + * Opens the naming series dialog for a doctype. + * + * @param {string} doctype - Transaction doctype. + * @param {Function} [on_update] - Called after series are updated. + * @returns {void} + */ + show_naming_series_dialog(doctype, on_update) { + if (!this.frm._naming_dialogs) this.frm._naming_dialogs = {}; + + if (!this.frm._naming_dialogs[doctype]) { + this.frm._naming_dialogs[doctype] = new erpnext.NamingSeriesDialog({ + doctype: doctype, + title: __("{0} Naming Series", [__(doctype)]), + on_update: ({ naming_series_options }) => { + const series = naming_series_options.split("\n").filter(Boolean); + this.frm + .get_field(this.opts.table_field) + .$wrapper.find(`.series-cell-${frappe.scrub(doctype)}`) + .html(this.frm._naming_series_table?.series_list_background(series)); + on_update?.({ doctype, naming_series_options }); + }, + }); + } + + this.frm._naming_dialogs[doctype].show(); + } +}; diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.js b/erpnext/selling/doctype/selling_settings/selling_settings.js index dfa2a45727d..ce22db3fff0 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.js +++ b/erpnext/selling/doctype/selling_settings/selling_settings.js @@ -3,27 +3,39 @@ frappe.ui.form.on("Selling Settings", { refresh(frm) { + if (!frm.naming_controller) frm.naming_controller = new erpnext.NamingSeriesController(frm); + const display = frm.doc.cust_master_name === "Naming Series"; frm.set_df_property("naming_series_details", "hidden", !display); frm.set_df_property("configure", "hidden", !display); + if (display) { - find_naming_series("Customer", "naming_series_details", frm); + frm.naming_controller.load_master_series("Customer", "naming_series_details"); } - load_default_naming_series(frm); + + frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm)); }, + cust_master_name(frm) { const display = frm.doc.cust_master_name === "Naming Series"; frm.set_df_property("naming_series_details", "hidden", !display); frm.set_df_property("configure", "hidden", !display); + if (display) { - find_naming_series("Customer", "naming_series_details", frm); + frm.naming_controller.load_master_series("Customer", "naming_series_details"); } else { - frm.set_value("naming_series_details", ""); + frm.doc.naming_series_details = ""; + frm.refresh_field("naming_series_details"); } + + frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm)); }, configure(frm) { - show_naming_series_dialog("Customer", frm); + frm.naming_controller.show_naming_series_dialog("Customer", ({ naming_series_options }) => { + frm.doc.naming_series_details = naming_series_options; + frm.refresh_field("naming_series_details"); + }); }, after_save(frm) { @@ -31,51 +43,18 @@ frappe.ui.form.on("Selling Settings", { }, }); -function show_naming_series_dialog(doctype, frm) { - if (!frm._naming_series_dialog) { - frm._naming_series_dialog = new erpnext.NamingSeriesDialog({ - doctype: doctype, - title: __("Naming Series for {0}", [__(doctype)]), - on_update: ({ naming_series_options }) => { - frm.set_value("naming_series_details", naming_series_options); - }, - }); - } - frm._naming_series_dialog.show(); -} -function find_naming_series(doctype, field, frm) { - frappe.model.with_doctype(doctype, () => { - const meta = frappe.get_meta(doctype); - const naming_df = (meta?.fields || []).find((df) => df.fieldname === "naming_series"); - const options = naming_df?.options || ""; - const series_list = options - .split("\n") - .map((s) => s.trim()) - .filter(Boolean); - - frm.doc[field] = series_list.length ? series_list.join("\n") : __("No naming series defined"); - - frm.refresh_field(field); - }); -} - -function load_default_naming_series(frm) { - let transactions = [ +function get_transactions(frm) { + const transactions = [ { label: __("Customer"), doctype: "Customer" }, { label: __("Quotation"), doctype: "Quotation" }, { label: __("Sales Order"), doctype: "Sales Order" }, { label: __("Sales Invoice"), doctype: "Sales Invoice" }, { label: __("Delivery Note"), doctype: "Delivery Note" }, - { label: __("Payment Entry"), doctype: "Payment Entry" }, - { label: __("POS Invoice"), doctype: "POS Invoice" }, ]; if (frm.doc.cust_master_name !== "Naming Series") { - transactions = transactions.filter((t) => t.doctype !== "Customer"); + return transactions.filter((t) => t.doctype !== "Customer"); } - new erpnext.NamingSeriesTable({ - frm: frm, - fieldname: "transaction_naming_html", - transactions: transactions, - }).render(); + + return transactions; }