Here is a Client Script for ERPNext (Quotation Item) that automatically computes and displays an item’s selling price based on:
- BOM Cost (if a BOM exists)
- Item Valuation Rate (if no BOM)
- Price List Rate (if no BOM or valuation)
- Pricing Rules (item-level, group-level, brand, or transaction-specific)
This script:
- Automatically applies the appropriate markup or margin from Pricing Rules.
- Displays a detailed breakdown:
- BOM reference (if any)
- Base cost used (BOM, valuation, or price list)
- Markup percentage and amount
- Final computed selling price
- Pricing Rule applied (name & type)
- Price List used
If no BOM or valuation rate is found, it gracefully falls back to the price list rate
How to Use:
- Go to Custom Script in ERPNext.
- Create a new script:
*Doctype: Quotation
- Script Type: Client Script
- Paste the script and save.
- Refresh your Quotation form.
Feel free to use and enhance the script and share it with the community.
frappe.ui.form.on(‘Quotation Item’, {
item_code: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (!row.item_code) return;
console.log("Selected Item:", row.item_code);
let price_list_name = frm.doc.selling_price_list || "Not Specified";
// Step 1: Try fetching BOM cost
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "BOM",
filters: { item: row.item_code, is_active: 1, is_default: 1 },
fields: ["name", "quantity", "total_cost"]
},
callback: function(bom_res) {
if (bom_res.message.length > 0) {
// BOM exists → use BOM cost
processPricing(bom_res.message[0].total_cost, bom_res.message[0].name);
} else {
// No BOM → fetch item valuation or standard selling price
frappe.call({
method: "frappe.client.get",
args: { doctype: "Item", name: row.item_code },
callback: function(item_res) {
let item = item_res.message;
let fallback_cost = item.valuation_rate || 0;
let bom_name = "No BOM (Using Item Valuation)";
if (!fallback_cost || fallback_cost === 0) {
// If no valuation, fallback to price list rate
frappe.call({
method: "erpnext.stock.get_item_details.get_item_details",
args: {
args: {
item_code: row.item_code,
price_list: frm.doc.selling_price_list,
transaction_date: frm.doc.transaction_date,
company: frm.doc.company,
qty: row.qty || 1,
doctype: "Quotation",
currency: frm.doc.currency || "PHP", // Force PHP
conversion_rate: 1 // Skip exchange rate
}
},
callback: function(price_res) {
fallback_cost = price_res.message?.price_list_rate || 0;
bom_name = "No BOM (Using Price List Rate)";
processPricing(fallback_cost, bom_name);
}
});
} else {
processPricing(fallback_cost, bom_name);
}
}
});
}
}
});
// Core pricing & rule logic
function processPricing(base_cost, bom_name) {
// Fetch item details (Item Group & Brand)
frappe.call({
method: "frappe.client.get",
args: { doctype: "Item", name: row.item_code },
callback: function(item_res) {
let item_group = item_res.message.item_group;
let brand = item_res.message.brand || null;
// Fetch pricing rules
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "Pricing Rule",
filters: { selling: 1, company: frm.doc.company },
fields: ["name", "apply_on", "margin_type", "margin_rate_or_amount", "priority"]
},
callback: function(price_res) {
let rules = price_res.message.sort((a, b) => (b.priority || 0) - (a.priority || 0));
let matched_rule = null;
function checkRule(index) {
if (index >= rules.length) {
computeAndDisplay(matched_rule, base_cost, bom_name);
return;
}
frappe.call({
method: "frappe.client.get",
args: { doctype: "Pricing Rule", name: rules[index].name },
callback: function(rule_detail) {
let doc = rule_detail.message;
// Match rules by priority: Item > Item Group > Brand > Transaction
if (!matched_rule && doc.apply_on === "Item Code" && doc.items?.some(i => i.item_code === row.item_code)) matched_rule = doc;
else if (!matched_rule && doc.apply_on === "Item Group" && doc.item_groups?.some(g => g.item_group === item_group)) matched_rule = doc;
else if (!matched_rule && doc.apply_on === "Brand" && doc.brand === brand) matched_rule = doc;
else if (!matched_rule && doc.apply_on === "Transaction" && doc.transaction === "Quotation") matched_rule = doc;
checkRule(index + 1);
}
});
}
checkRule(0);
}
});
}
});
}
function computeAndDisplay(rule, base_cost, bom_name) {
let markup_percentage = 0;
let rule_name = "No Pricing Rule Applied";
let rule_type = "";
if (rule) {
rule_name = rule.name;
rule_type = rule.apply_on;
if (rule.margin_type === "Percentage") {
markup_percentage = rule.margin_rate_or_amount;
} else if (rule.margin_type === "Amount") {
markup_percentage = (rule.margin_rate_or_amount / base_cost) * 100;
}
}
let markup_amount = base_cost * (markup_percentage / 100);
let selling_price = base_cost + markup_amount;
// Show popup message
frappe.msgprint({
title: __('BOM & Price Computation'),
message: `
<b>BOM:</b> ${bom_name}<br>
<b>Base Cost:</b> ₱${base_cost.toFixed(2)}<br>
<b>Markup:</b> ${markup_percentage}% = ₱${markup_amount.toFixed(2)}<br>
<b>Computed Selling Price:</b> ₱${selling_price.toFixed(2)}<br><br>
<b>Applied Pricing Rule:</b> ${rule_name} ${rule_type ? "(" + rule_type + ")" : ""}<br>
<b>Price List Used:</b> ${price_list_name}
`,
indicator: 'blue'
});
// Auto-fill rate field in Quotation
frappe.model.set_value(cdt, cdn, "rate", selling_price);
}
}
});