fix(UX): Buying settings form cleanup (#54731)

* fix(UX): Buying settings form cleanup

* fix: controller approach modification

* fix: dark mode support
This commit is contained in:
Nishka Gosalia
2026-05-13 14:53:04 +05:30
committed by GitHub
parent cf5e8ce878
commit 45f05fbeaa
5 changed files with 289 additions and 109 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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'

View File

@@ -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 `<span class="text-muted">${__("Not configured")}</span>`;
}
const badge_class = this.get_current_badge_class();
return series_list
.map(
(s) => `<span class="badge badge-light"
(s) => `<span class="badge ${badge_class}"
style="margin: 2px; font-family: monospace; font-weight: normal;">
${frappe.utils.escape_html(s)}
</span>`
@@ -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();
}
};

View File

@@ -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;
}