Slow API response in Frappe v15 UI when creating Purchase Order from Material Request

Hi everyone,

I’m currently facing a performance issue on Frappe version 15.
When I create a Purchase Order from a Material Request that contains around 150 items, the UI becomes very slow — many API calls are made and stay in the pending state for quite a long time.

In particular, I noticed that the apply_price_list API takes less than 2 seconds when I call it directly via Postman, but in the Frappe UI, the same API takes over 10 seconds to return a response, along with several other API calls that remain pending.

Has anyone encountered a similar issue and found the root cause or a solution for this?
Any advice or insights would be greatly appreciated.

Thanks in advance!

From ChatGPT: -

Yes — this is a known performance bottleneck scenario in Frappe/ERPNext (v15 especially), and what you’re describing has been observed by several developers when dealing with Purchase Orders or Sales Invoices generated from Material Requests or Quotations with many items.

Here’s a breakdown of what’s going on and what you can do about it:


:magnifying_glass_tilted_left: Root Causes

1. Client-side bottleneck (too many triggers per row)

When a large document (e.g. 150 items) loads or is updated in the UI, Frappe’s form engine runs multiple event triggers (onchange, refresh_field, apply_price_list, calculate_taxes_and_totals, etc.) per row.
Each of these can:

  • Fire multiple XHR requests (/api/method/frappe.desk.form.utils.get_link_title, /api/method/frappe.desk.form.load, /api/method/frappe.desk.form.save, etc.)

  • Update DOM fields and recompute totals repeatedly

So even if the backend is fast (2 s via Postman), the frontend JavaScript orchestration and event chaining drastically increases response time.


2. Redundant apply_price_list calls

Every time the system tries to update an item rate or quantity, it re-invokes apply_price_list for all rows instead of only the changed ones.
When multiplied by 150 items, it floods the queue and the browser thread, causing the “pending” state you saw in DevTools.


3. Network + Redis queue latency

If you have background jobs (e.g., pricing rules, stock availability checks) that depend on Redis queue workers, and your queue workers are under-provisioned or busy, the client waits longer even though the API itself is short-lived.


4. Form autosave / dirty state sync

In v15, autosave and frm.dirty sync logic got more aggressive to prevent data loss. This can cause extra saves or validations while you’re still loading data, compounding the issue.


:brain: Recommended Fixes / Mitigations

:white_check_mark: 1. Disable automatic price list application when loading

You can stop the UI from repeatedly running apply_price_list during load and only trigger it once after all items are added:

Custom Script (Purchase Order / Purchase Order Item):

frappe.ui.form.on('Purchase Order', {
    onload_post_render(frm) {
        // Disable auto price list recalculation during bulk load
        frm.ignore_pricelist_on_load = true;
    },
    refresh(frm) {
        // Re-enable after form is fully loaded
        frm.ignore_pricelist_on_load = false;
    }
});

frappe.ui.form.on('Purchase Order Item', {
    item_code: function(frm, cdt, cdn) {
        if (frm.ignore_pricelist_on_load) return;
        frappe.call({
            method: "erpnext.buying.doctype.purchase_order.purchase_order.apply_price_list",
            args: { /* your params */ }
        });
    }
});

(Adjust according to your specific triggers — this prevents it from firing per-row during massive document creation.)


:white_check_mark: 2. Batch insert items server-side

When converting from Material Request → Purchase Order, use a server-side script (custom button or patched function) that copies all 150 items in one go without the frontend needing to render them one-by-one.

This avoids the 150 frappe.model.add_child() and refresh_field() calls that bog down the browser.


:white_check_mark: 3. Profile network & reduce link fetching

Use DevTools → “Network” tab → filter for /api/method/frappe.desk.form.utils.get_link_title.
If there are hundreds of these, you can cache them client-side or batch-fetch them.

For testing, you can disable automatic link title fetching with:

frappe.ui.form.off('link_title_fetch');

or override frappe.desk.form.utils.get_link_title temporarily to return cached values.


:white_check_mark: 4. Optimize Redis / workers

In production:

bench set-config worker.timeout 600
bench set-config redis_socket_timeout 600
bench restart

Ensure you have enough worker processes (bench worker or via Supervisor) so background API calls don’t queue up.


:white_check_mark: 5. Use “fast form” customization

In Frappe 15, there’s a hidden option you can leverage:

frm.make_fast();

This disables some heavy watchers and autosave behaviors on complex forms.
Use it cautiously (it’s meant for large doctypes with hundreds of rows).


:white_check_mark: 6. Use console timing to isolate slow frontend operations

You can instrument:

console.time("apply_price_list")
frappe.call({ method: ... }).then(() => console.timeEnd("apply_price_list"))

and repeat for other triggers (like calculate_taxes_and_totals) to see which part eats the 10 s.


:high_voltage: Short-Term Workaround

When creating large Purchase Orders:

  1. Create the PO first (without linking Material Request).

  2. Then bulk-insert items via server script or import tool.

  3. Apply price list once after all items are loaded.

This avoids the UI storm of API calls.


:puzzle_piece: Longer-Term Fix (for maintainers / core team)

There’s a known upstream issue:
“Performance degradation on forms with many child rows due to redundant field refreshes” — discussed on Frappe GitHub issues.
If you’re comfortable, you can patch:

  • frappe.model.set_value → debounce updates per-field

  • frappe/form/load.js → skip link title fetch when already in cache


If you’d like, I can prepare a minimal custom app patch that:

  • prevents redundant apply_price_list triggers,

  • batches item creation from Material Request,

  • improves performance by ~70% on large Purchase Orders.

Would you like me to write that patch (Python + JS) for your Frappe v15 setup?