From 844f3dbc0b05f885488759fc4dc6b92e3f151892 Mon Sep 17 00:00:00 2001 From: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:45:10 +0530 Subject: [PATCH] feat(ux): Naming series dialog (#54554) --- erpnext/public/js/erpnext.bundle.js | 1 + .../public/js/utils/naming_series_dialog.js | 312 ++++++++++++++++++ .../selling_settings/selling_settings.js | 73 ++++ .../selling_settings/selling_settings.json | 35 +- 4 files changed, 417 insertions(+), 4 deletions(-) create mode 100644 erpnext/public/js/utils/naming_series_dialog.js diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 221e3a62c6a..eca43de6f83 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -37,5 +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 { 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_dialog.js new file mode 100644 index 00000000000..b7f1ff076a0 --- /dev/null +++ b/erpnext/public/js/utils/naming_series_dialog.js @@ -0,0 +1,312 @@ +frappe.provide("erpnext"); + +erpnext.NamingSeriesDialog = class NamingSeriesDialog { + constructor(opts = {}) { + this.opts = Object.assign( + { + title: __("Document Naming"), + single_doctype: "Document Naming Settings", + }, + opts + ); + + this.current_doctype = null; + this.loaded = false; + this.make_dialog(); + } + + make_dialog() { + this.dialog = new frappe.ui.Dialog({ + title: this.opts.title, + size: "medium", + fields: [ + { + fieldtype: "Table", + fieldname: "naming_series_options", + label: __("Add Series Prefix"), + reqd: 1, + in_place_edit: true, + data: [], + fields: [ + { + fieldtype: "Data", + fieldname: "series", + label: __("Series"), + in_list_view: 1, + change: async function () { + const preview = await this.grid_row.grid._naming_dialog.get_series_preview( + this.doc.series + ); + this.doc.preview = preview; + this.grid_row.refresh_field("preview"); + }, + }, + { + fieldtype: "Data", + fieldname: "preview", + label: __("Preview"), + in_list_view: 1, + placeholder: " ", + read_only: 1, + }, + ], + }, + { fieldtype: "Section Break", label: __("Rules for configuring series"), collapsible: 1 }, + { + fieldtype: "HTML", + fieldname: "naming_series_description", + }, + ], + primary_action_label: __("Update"), + primary_action: () => this.save(), + }); + + this.dialog.fields_dict.naming_series_options.grid._naming_dialog = this; + } + + async show() { + this.dialog.show(); + this.render_help(); + + if (this.opts.doctype && !this.loaded) { + await this.get_transaction(this.opts.doctype); + this.loaded = true; + return; + } + } + + render_help() { + this.dialog.get_field("naming_series_description").$wrapper.html(` + + Examples: + +
`); + } + + get_series_preview(series) { + if (!series) return ""; + + return this.get_document_naming_doc().then((doc) => { + doc.try_naming_series = series; + doc.transaction_type = this.current_doctype; + return frappe + .call({ + doc: doc, + method: "preview_series", + freeze: true, + }) + .then((r) => (r.message || "").split("\n")[0] || ""); + }); + } + + get_document_naming_doc() { + const dt = this.opts.single_doctype; + return frappe.model.with_doc(dt, dt).then(() => { + return frappe.model.get_doc(dt, dt); + }); + } + + async get_transaction(doctype) { + this.current_doctype = doctype; + + await frappe.model.with_doctype(doctype, async () => { + const meta = frappe.get_meta(doctype); + const naming_df = (meta?.fields || []).find((df) => df.fieldname === "naming_series"); + const series_list = (naming_df?.options || "").split("\n").filter(Boolean); + const rows = await Promise.all( + series_list.map(async (series) => ({ + series: series, + preview: await this.get_series_preview(series), + })) + ); + + this.dialog.fields_dict.naming_series_options.df.data = rows; + this.dialog.fields_dict.naming_series_options.grid.refresh(); + }); + } + + save() { + const rows = this.dialog.fields_dict.naming_series_options.grid.get_data(); + const naming_series_options = rows + .map((r) => (r.series || "").trim()) + .filter(Boolean) + .join("\n"); + + if (!this.current_doctype) { + frappe.msgprint(__("Please select a transaction.")); + return; + } + + if (!naming_series_options) { + frappe.msgprint(__("Please add at least one naming series.")); + return; + } + + this.get_document_naming_doc().then((doc) => { + doc.transaction_type = this.current_doctype; + doc.naming_series_options = naming_series_options; + + frappe.call({ + doc: doc, + method: "update_series", + freeze: true, + callback: async () => { + const updated_rows = await Promise.all( + naming_series_options + .split("\n") + .filter(Boolean) + .map(async (series) => ({ + series: series, + preview: await this.get_series_preview(series), + })) + ); + + this.dialog.fields_dict.naming_series_options.df.data = updated_rows; + this.dialog.fields_dict.naming_series_options.grid.refresh(); + + frappe.show_alert({ message: __("Naming Series updated"), indicator: "green" }); + this.dialog.hide(); + this.opts.on_update?.({ doctype: this.current_doctype, naming_series_options }); + }, + }); + }); + } +}; + +erpnext.NamingSeriesTable = class NamingSeriesTable { + constructor(opts = {}) { + this.frm = opts.frm; + this.transactions = opts.transactions || []; + this.$wrapper = opts.frm.get_field(opts.fieldname).$wrapper; + } + render() { + this.$wrapper.html(` +
+ + + + + + + + +
+ ${__("Transaction")} + + ${__("Current Series")} +
+
+ `); + + const $rows = this.$wrapper.find(".naming-series-table-rows"); + this.map_configure_button($rows); + this.get_row_data($rows); + } + + map_configure_button($rows) { + $rows.on("click", ".configure-btn", (e) => { + const $btn = $(e.currentTarget); + const doctype = $btn.data("doctype"); + const label = $btn.data("label"); + + 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", [__(label)]), + on_update: ({ naming_series_options }) => { + const series = naming_series_options.split("\n").filter(Boolean); + this.$wrapper + .find(`.series-cell-${frappe.scrub(doctype)}`) + .html(this.series_list_background(series)); + }, + }); + } + + this.frm._naming_dialogs[doctype].show(); + }); + } + + get_row_data($rows) { + this.transactions.forEach((t) => { + frappe.model.with_doctype(t.doctype, () => { + const meta = frappe.get_meta(t.doctype); + const naming_df = (meta?.fields || []).find((df) => df.fieldname === "naming_series"); + const series = (naming_df?.options || "") + .split("\n") + .map((s) => s.trim()) + .filter(Boolean); + + $rows.append(this.make_row(t, series)); + }); + }); + } + + make_row(t, series) { + return $(` + + + ${frappe.utils.escape_html(t.label)} + + + ${this.series_list_background(series)} + + + + ${frappe.utils.icon("edit", "sm")} + + + + `); + } + + series_list_background(series_list) { + if (!series_list.length) { + return `${__("Not configured")}`; + } + return series_list + .map( + (s) => ` + ${frappe.utils.escape_html(s)} + ` + ) + .join(""); + } +}; diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.js b/erpnext/selling/doctype/selling_settings/selling_settings.js index f7670e69d47..dfa2a45727d 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.js +++ b/erpnext/selling/doctype/selling_settings/selling_settings.js @@ -2,7 +2,80 @@ // For license information, please see license.txt frappe.ui.form.on("Selling Settings", { + refresh(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); + } + load_default_naming_series(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); + } else { + frm.set_value("naming_series_details", ""); + } + }, + + configure(frm) { + show_naming_series_dialog("Customer", frm); + }, + after_save(frm) { frappe.boot.user.defaults.editable_price_list_rate = frm.doc.editable_price_list_rate; }, }); + +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 = [ + { 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"); + } + new erpnext.NamingSeriesTable({ + frm: frm, + fieldname: "transaction_naming_html", + transactions: transactions, + }).render(); +} diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index bd5cea337a1..ebae841dde9 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -9,8 +9,10 @@ "customer_defaults_tab", "customer_defaults_section", "cust_master_name", - "customer_group", + "naming_series_details", + "configure", "column_break_4", + "customer_group", "territory", "item_price_tab", "item_price_settings_section", @@ -57,7 +59,9 @@ "section_break_zwh6", "allow_delivery_of_overproduced_qty", "column_break_mla9", - "deliver_secondary_items" + "deliver_secondary_items", + "default_naming_tab", + "transaction_naming_html" ], "fields": [ { @@ -279,7 +283,7 @@ { "fieldname": "item_price_tab", "fieldtype": "Tab Break", - "label": "Item Price" + "label": "Pricing" }, { "fieldname": "transaction_tab", @@ -377,6 +381,29 @@ "fieldname": "blanket_orders_section", "fieldtype": "Section Break", "label": "Blanket Orders" + }, + { + "fieldname": "configure", + "fieldtype": "Button", + "hidden": 1, + "label": "Configure Series" + }, + { + "fieldname": "naming_series_details", + "fieldtype": "Small Text", + "hidden": 1, + "is_virtual": 1, + "label": "Naming Series options", + "read_only": 1 + }, + { + "fieldname": "default_naming_tab", + "fieldtype": "Tab Break", + "label": "Document Naming" + }, + { + "fieldname": "transaction_naming_html", + "fieldtype": "HTML" } ], "grid_page_length": 50, @@ -385,7 +412,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-04-21 21:29:32.890098", + "modified": "2026-04-29 11:05:48.836362", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings",