mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-07 15:25:19 +00:00
feat(ux): Naming series dialog (#54554)
(cherry picked from commit 844f3dbc0b)
This commit is contained in:
@@ -37,5 +37,6 @@ import "./utils/demo.js";
|
|||||||
import "./financial_statements.js";
|
import "./financial_statements.js";
|
||||||
import "./sales_trends_filters.js";
|
import "./sales_trends_filters.js";
|
||||||
import "./purchase_trends_filters.js";
|
import "./purchase_trends_filters.js";
|
||||||
|
import "./utils/naming_series_dialog.js";
|
||||||
|
|
||||||
// import { sum } from 'frappe/public/utils/util.js'
|
// import { sum } from 'frappe/public/utils/util.js'
|
||||||
|
|||||||
312
erpnext/public/js/utils/naming_series_dialog.js
Normal file
312
erpnext/public/js/utils/naming_series_dialog.js
Normal file
@@ -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(`
|
||||||
|
<ul>
|
||||||
|
<li>${__("Allowed special characters are '/' and '-'")}</li>
|
||||||
|
<li>
|
||||||
|
${__(
|
||||||
|
"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."
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li> ${__("You can also use variables in the series name by putting them between (.) dots")}
|
||||||
|
<br>
|
||||||
|
${__("Supported Variables:")}
|
||||||
|
<ul>
|
||||||
|
<li><code>.YYYY.</code> - ${__("Year in 4 digits")}</li>
|
||||||
|
<li><code>.YY.</code> - ${__("Year in 2 digits")}</li>
|
||||||
|
<li><code>.MM.</code> - ${__("Month")}</li>
|
||||||
|
<li><code>.DD.</code> - ${__("Day of month")}</li>
|
||||||
|
<li><code>.WW.</code> - ${__("Week of the year")}</li>
|
||||||
|
<li>
|
||||||
|
<code>.{fieldname}.</code> - ${__("fieldname on the document e.g.")}
|
||||||
|
<code>branch</code>
|
||||||
|
</li>
|
||||||
|
<li><code>.FY.</code> - ${__("Fiscal Year (requires ERPNext to be installed)")}</li>
|
||||||
|
<li><code>.ABBR.</code> - ${__("Company Abbreviation (requires ERPNext to be installed)")}</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
Examples:
|
||||||
|
<ul>
|
||||||
|
<li>INV-</li>
|
||||||
|
<li>INV-10-</li>
|
||||||
|
<li>INVK-</li>
|
||||||
|
<li>INV-.YYYY.-._{branch}.-.MM.-.####</li>
|
||||||
|
</ul>
|
||||||
|
<br>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(`
|
||||||
|
<div class="form-grid" style="margin-bottom: 24px;">
|
||||||
|
<table class="table" style="margin: 0;">
|
||||||
|
<thead class="grid-heading-row" style="background-color: var(--subtle-fg);">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 25%; padding: 8px 12px; text-align: left;">
|
||||||
|
${__("Transaction")}
|
||||||
|
</td>
|
||||||
|
<td colspan="2"
|
||||||
|
style="width: 75%; padding: 8px 12px; text-align: left; border-left: 1px solid var(--border-color);">
|
||||||
|
${__("Current Series")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="naming-series-table-rows"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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 $(`
|
||||||
|
<tr>
|
||||||
|
<td style="width: 25%; padding: 8px 12px; vertical-align: top; background-color: var(--card-bg);">
|
||||||
|
${frappe.utils.escape_html(t.label)}
|
||||||
|
</td>
|
||||||
|
<td class="series-cell-${frappe.scrub(t.doctype)}"
|
||||||
|
style="width: 70%; padding: 8px 12px; border-left: 1px solid var(--border-color); white-space: normal; vertical-align: top; background-color: var(--card-bg);">
|
||||||
|
${this.series_list_background(series)}
|
||||||
|
</td>
|
||||||
|
<td class="text-center"
|
||||||
|
style="width: 5%; padding: 8px 12px; border-left: 1px solid var(--border-color); vertical-align: middle; background-color: var(--card-bg);">
|
||||||
|
<a class="btn-link configure-btn"
|
||||||
|
data-doctype="${frappe.utils.escape_html(t.doctype)}"
|
||||||
|
data-label="${frappe.utils.escape_html(t.label)}"
|
||||||
|
style="cursor: pointer; color: var(--text-muted);">
|
||||||
|
${frappe.utils.icon("edit", "sm")}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
series_list_background(series_list) {
|
||||||
|
if (!series_list.length) {
|
||||||
|
return `<span class="text-muted">${__("Not configured")}</span>`;
|
||||||
|
}
|
||||||
|
return series_list
|
||||||
|
.map(
|
||||||
|
(s) => `<span class="badge badge-light"
|
||||||
|
style="margin: 2px; font-family: monospace; font-weight: normal;">
|
||||||
|
${frappe.utils.escape_html(s)}
|
||||||
|
</span>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,7 +2,80 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("Selling Settings", {
|
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) {
|
after_save(frm) {
|
||||||
frappe.boot.user.defaults.editable_price_list_rate = frm.doc.editable_price_list_rate;
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,8 +9,10 @@
|
|||||||
"customer_defaults_tab",
|
"customer_defaults_tab",
|
||||||
"customer_defaults_section",
|
"customer_defaults_section",
|
||||||
"cust_master_name",
|
"cust_master_name",
|
||||||
"customer_group",
|
"naming_series_details",
|
||||||
|
"configure",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
|
"customer_group",
|
||||||
"territory",
|
"territory",
|
||||||
"item_price_tab",
|
"item_price_tab",
|
||||||
"item_price_settings_section",
|
"item_price_settings_section",
|
||||||
@@ -57,7 +59,9 @@
|
|||||||
"section_break_zwh6",
|
"section_break_zwh6",
|
||||||
"allow_delivery_of_overproduced_qty",
|
"allow_delivery_of_overproduced_qty",
|
||||||
"column_break_mla9",
|
"column_break_mla9",
|
||||||
"deliver_secondary_items"
|
"deliver_secondary_items",
|
||||||
|
"default_naming_tab",
|
||||||
|
"transaction_naming_html"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -279,7 +283,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "item_price_tab",
|
"fieldname": "item_price_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Item Price"
|
"label": "Pricing"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "transaction_tab",
|
"fieldname": "transaction_tab",
|
||||||
@@ -377,6 +381,29 @@
|
|||||||
"fieldname": "blanket_orders_section",
|
"fieldname": "blanket_orders_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Blanket Orders"
|
"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,
|
"grid_page_length": 50,
|
||||||
@@ -385,7 +412,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-04-21 21:29:32.890098",
|
"modified": "2026-04-29 11:05:48.836362",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Selling Settings",
|
"name": "Selling Settings",
|
||||||
|
|||||||
Reference in New Issue
Block a user