Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e24ab72
fix: copy project from first row to new rows (backport #53295) (#54620)
mergify[bot] Apr 29, 2026
9db03bc
fix(selling): blanket order ordered qty recalculation on sales order …
mergify[bot] Apr 29, 2026
d6f2ff6
fix: show correct status in Serial No Ledger (backport #54567) (#54626)
mergify[bot] Apr 29, 2026
808214f
perf: max recursion depth error in serial no (backport #54629) (#54631)
mergify[bot] Apr 29, 2026
48ebb4c
feat(ux): Naming series dialog (#54554)
nishkagosalia Apr 29, 2026
2e43801
Merge pull request #54635 from frappe/mergify/bp/version-16-hotfix/pr…
nishkagosalia Apr 29, 2026
7bd360a
fix: py error on sales forecast doctype (backport #54641) (#54643)
mergify[bot] Apr 29, 2026
6dbc17d
fix: dont show serial/batch button when PR is submitted (backport #54…
mergify[bot] Apr 29, 2026
19a8ebe
fix(payment_entry): convert the date args to string type before escap…
mergify[bot] Apr 29, 2026
86cf256
fix: correct project filter in buying doctypes (backport #54644) (#54…
mergify[bot] Apr 29, 2026
b300159
fix: use RecoverableErrors isinstance check for repost timeout status…
mergify[bot] Apr 29, 2026
d3c893d
fix: skip depreciation rescheduling when asset is fully depreciated o…
khushi8112 Apr 29, 2026
07a957c
fix: skip rescheduling only for asset being disposed
khushi8112 Apr 29, 2026
bd932da
feat: copy terms attachments to transactions (backport #53403) (#54661)
mergify[bot] Apr 29, 2026
c232f1f
Merge pull request #54659 from frappe/mergify/bp/version-16-hotfix/pr…
khushi8112 Apr 30, 2026
d27cf48
fix: show in and out qty in the stock ledger report for stock recos
rohitwaghchaure Apr 30, 2026
38cfeb1
fix: correct titles set to {customer_name} or {supplier_name} text st…
mergify[bot] Apr 30, 2026
2422237
Merge pull request #54671 from frappe/mergify/bp/version-16-hotfix/pr…
rohitwaghchaure Apr 30, 2026
288cdf3
fix(project): use user.email for invitations and skip disabled users.…
mergify[bot] Apr 30, 2026
126e13b
fix: mark item tax templates as not applicable (backport #54673) (#54…
mergify[bot] Apr 30, 2026
a22d773
fix: Backfill `not_applicable` on Item Tax Template Details for Germa…
mergify[bot] May 1, 2026
0dade2c
fix: incorrect expense account book in purchase return (backport #546…
mergify[bot] May 1, 2026
bca893a
fix: add missing fields in set_currency_labels (backport #54689) (#54…
mergify[bot] May 1, 2026
bbb4e79
fix: set valid_from in created Item Price (backport #54696) (#54700)
mergify[bot] May 2, 2026
18006b9
chore: update POT file (#54710)
frappe-pr-bot May 3, 2026
982810a
fix: accounts and account types in German CoA "SKR 03" (backport #547…
mergify[bot] May 3, 2026
2cd4c1a
fix: error when creating quotation from CRM (backport #54722) (#54725)
mergify[bot] May 4, 2026
e60490d
fix: hide payment and payment request buttons based on permissions in…
mergify[bot] May 5, 2026
0f27881
fix: Remove bom stock report link from manufacturing workspace
nishkagosalia May 5, 2026
c985f94
Merge pull request #54743 from frappe/mergify/bp/version-16-hotfix/pr…
nishkagosalia May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions erpnext/public/js/erpnext.bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
312 changes: 312 additions & 0 deletions erpnext/public/js/utils/naming_series_dialog.js
Original file line number Diff line number Diff line change
@@ -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("");
}
};
Loading