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(`
+
+ - ${__("Allowed special characters are '/' and '-'")}
+ -
+ ${__(
+ "Optionally, set the number of digits in the series using dot (.) followed by hashes (#). For example, '.####' means that the series will have four digits. Default is five digits."
+ )}
+
+ - ${__("You can also use variables in the series name by putting them between (.) dots")}
+
+ ${__("Supported Variables:")}
+
+ .YYYY. - ${__("Year in 4 digits")}
+ .YY. - ${__("Year in 2 digits")}
+ .MM. - ${__("Month")}
+ .DD. - ${__("Day of month")}
+ .WW. - ${__("Week of the year")}
+ -
+
.{fieldname}. - ${__("fieldname on the document e.g.")}
+ branch
+
+ .FY. - ${__("Fiscal Year (requires ERPNext to be installed)")}
+ .ABBR. - ${__("Company Abbreviation (requires ERPNext to be installed)")}
+
+
+
+ Examples:
+
+ - INV-
+ - INV-10-
+ - INVK-
+ - INV-.YYYY.-._{branch}.-.MM.-.####
+
+
`);
+ }
+
+ 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(`
+
+ `);
+
+ 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",