BOM cost VS item cost

please clarify how ERP NEXT wants us to use the following:

  1. BOM for Item-A shows a cost to manufacture Item-A.
  2. A sales quotation CANNOT reference the BOM cost, instead it looks at the
    selling price list for Item-A
  3. Are we supposed to manually REPLACE the selling price each time BOM cost
    changes?

Ever tried Price Lists?

Yes that is what I meant in point no. 2
this is considered manual updating. imagine updating 100’s of prices manually.
I feel the BOM calculated cost must auto update the pricelist?

BOM cost and Sales Price are unrelated in ERPNext. To be honest, I haven’t seen any company implementing a direct relation on sales prices from their BOM costs on any erp.

In manufacturing you do that for every single item. so, unless I understand BOM wrong,
there should be a direct link ALWAYS.
Hence the original topic question.

In manufacturing you have to quote the customer before he places the order.
the only way to quote a customer is with building the BOM. Do you see where this is going?

Do you mean that a company sells each and every Item manufactured on a simple margin rate? For all of their products? What if the items in stock manufactured last month and the manuf.costs/direct, indirect costs are updated.
Makes no sense… There is always human intervention there. Unless, you are an FMCG company which is not producing anything but box moving…

We sell thousands of items manufactured.
when raw material prices go up or down, the selling price must change.
so, I must manually change thousands of parts 3 to four times a year as the raw material price fluctuates.
this is considered standard. Or how would you suggest handling this situation?

If the price of the item is calculated on market values such as London Metals Exchange, you need to incorporate them with a custom design and develop the necessary updating code. In case of internal price change calculations, there are endless limitations:

  1. BOM cost calculation uses a limited source of data
  2. BOM updates should be triggered in your case
  3. Would you like to keep a steady margin rate over costs of all manufactured items?
    In any case this calls for a custom development if you are getting the business process correcly.

Hi,

You may set the sales pricing rule margin based, let’s say you want 20% minimum margin on the cost. You can set the pricing rule for that, and the system will automatically adjust the pricing based on the cost of the product.

Thanks,

Divyesh M.

1 Like

Surely, that is one of the ways but not 100% dependable. BOM costing scheme has limitations. Unless you find a solution or approach to that, the dynamic condition you set will work but calculate based on data at the time of sales. You will not know if it reflects all the input updates.

YES. it seems that customising is the ideal route to go.

will it be difficult to do the following with client script:

when opening an ITEM.
display
- the linked BOM’S COST
- the linked selling price list price
- add a button that replaces the sell price with the BOM’S COST + certain %

this way we have a quick reference if the selling price is to low or still ok for selling

1 Like

This is it, this is pretty obvious and a solution that should have been implemented.

Those who transform thousands of different options of variants must have a way to calculate the cost of the product based on the manufactoring cost. If eletricity goes up, we just change the eletricity cost and that should reflect on the price of the product.

I’m trying now to solve this on my end.

Please note that, ERPNext has the ability to warn the user if you net sales price in below the cost of the item, that is you are making loss on a sale of an item… that is huge!

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:
    1. BOM reference (if any)
    2. Base cost used (BOM, valuation, or price list)
    3. Markup percentage and amount
    4. Final computed selling price
    5. Pricing Rule applied (name & type)
    6. Price List used

If no BOM or valuation rate is found, it gracefully falls back to the price list rate

How to Use:

  1. Go to Custom Script in ERPNext.
  2. Create a new script:
    *Doctype: Quotation
  • Script Type: Client Script
  1. Paste the script and save.
  2. 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);
    }
}

});