From 5873f55cf059e28faa9acd7db1607a64f3ffd4e8 Mon Sep 17 00:00:00 2001
From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com>
Date: Fri, 5 Jun 2026 15:42:18 +0530
Subject: [PATCH] feat: item prices list view (#54853)
* feat: add item prices tab to Item doctype
* feat: item form pricing tab
* fix: remove action button for edit item price
* fix: prevent stale item price rendering after form navigation
* fix: remove stale call to deleted edit_prices_button function
* fix: item price list fixes
* fix: show filtered price list
* fix: show filtered price list
---
erpnext/stock/doctype/item/item.js | 67 +++++++--
erpnext/stock/doctype/item/item.json | 23 ++-
erpnext/stock/doctype/item/item.py | 38 +++++
erpnext/stock/doctype/item/item_prices.html | 139 ++++++++++++++++++
.../stock/doctype/item_price/item_price.js | 25 ++++
5 files changed, 276 insertions(+), 16 deletions(-)
create mode 100644 erpnext/stock/doctype/item/item_prices.html
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 2261cb7733f..898708962a9 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -255,13 +255,14 @@ frappe.ui.form.on("Item", {
);
}
- erpnext.item.edit_prices_button(frm);
erpnext.item.toggle_attributes(frm);
if (!frm.doc.is_fixed_asset) {
erpnext.item.make_dashboard(frm);
}
+ erpnext.item.render_item_prices(frm);
+
frm.add_custom_button(__("Duplicate"), function () {
var new_item = frappe.model.copy_doc(frm.doc);
// Duplicate item could have different name, causing "copy paste" error.
@@ -665,24 +666,60 @@ $.extend(erpnext.item, {
}
},
- edit_prices_button: function (frm) {
- frm.add_custom_button(
- __("Add / Edit Prices"),
- function () {
- frappe.set_route("List", "Item Price", { item_code: frm.doc.name });
- },
- __("Actions")
+ render_item_prices: function (frm) {
+ if (frm.doc.__islocal) return;
+ const requested_item = frm.doc.name;
+ const container = frm.fields_dict["prices_html"].$wrapper;
+
+ container.html(
+ `
${__("Loading...")}
`
);
- frm.add_custom_button(
- __("Make Lead Time"),
- function () {
- frm.make_new("Item Lead Time", {
- item_code: frm.doc.name,
+ frappe.call({
+ method: "erpnext.stock.doctype.item.item.get_item_prices",
+ args: { item_code: requested_item },
+
+ callback: function (r) {
+ if (requested_item !== frm.doc.name) return;
+
+ if (!r.message) return;
+
+ const { prices, has_more } = r.message;
+
+ const html = frappe.render_template("item_prices", {
+ prices,
+ has_more,
+ item_code: requested_item,
+ stock_uom: frm.doc.stock_uom,
+ });
+
+ container.html(html);
+
+ container.find(".add-price-btn").on("click", () => {
+ const filters = {};
+ if (frm.doc.is_sales_item && !frm.doc.is_purchase_item) {
+ filters.selling = 1;
+ } else if (frm.doc.is_purchase_item && !frm.doc.is_sales_item) {
+ filters.buying = 1;
+ }
+ frappe.new_doc(
+ "Item Price",
+ { item_code: requested_item, uom: frm.doc.stock_uom },
+ (dialog) => {
+ if (Object.keys(filters).length) {
+ dialog.fields_dict.price_list.get_query = () => ({ filters });
+ }
+ }
+ );
+ });
+
+ container.find(".price-row").on("click", function (e) {
+ if ($(e.target).is("a")) return;
+
+ frappe.set_route("Form", "Item Price", $(this).data("name"));
});
},
- __("Actions")
- );
+ });
},
weight_to_validate: function (frm) {
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 8b4cef2b75c..ed7886d6be3 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -16,6 +16,7 @@
"item_name",
"item_group",
"stock_uom",
+ "image",
"column_break0",
"disabled",
"is_stock_item",
@@ -39,7 +40,6 @@
"over_delivery_receipt_allowance",
"column_break_wugd",
"over_billing_allowance",
- "image",
"section_break_11",
"brand",
"description",
@@ -138,6 +138,9 @@
"inspection_required_before_delivery",
"column_break_pxjh",
"quality_inspection_template",
+ "pricing_tab",
+ "item_prices_column",
+ "prices_html",
"dashboard_tab"
],
"fields": [
@@ -256,6 +259,7 @@
"description": "Used to create an opening Stock Entry with the Valuation Rate when the item is saved",
"fieldname": "opening_stock",
"fieldtype": "Float",
+ "hidden": 1,
"label": "Opening Stock"
},
{
@@ -1064,6 +1068,23 @@
{
"fieldname": "column_break_kpmi",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "prices_html",
+ "fieldtype": "HTML",
+ "label": "Prices HTML",
+ "options": ""
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "pricing_tab",
+ "fieldtype": "Tab Break",
+ "label": "Pricing"
+ },
+ {
+ "fieldname": "item_prices_column",
+ "fieldtype": "Column Break",
+ "label": "Item Prices"
}
],
"icon": "fa fa-tag",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 386877496f8..01d8a9d1e1a 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -1544,3 +1544,41 @@ def get_child_warehouses(warehouse):
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
return get_child_warehouses(warehouse)
+
+
+@frappe.whitelist()
+def get_item_prices(item_code: str):
+ """Fetch valid item prices for the item prices tab."""
+ if not frappe.has_permission("Item Price", "read"):
+ frappe.throw(_("Not permitted"), frappe.PermissionError)
+ today = getdate()
+
+ ItemPrice = frappe.qb.DocType("Item Price")
+
+ prices = (
+ frappe.qb.from_(ItemPrice)
+ .select(
+ ItemPrice.name,
+ ItemPrice.price_list,
+ ItemPrice.price_list_rate,
+ ItemPrice.currency,
+ ItemPrice.uom,
+ ItemPrice.customer,
+ ItemPrice.supplier,
+ ItemPrice.buying,
+ ItemPrice.selling,
+ ItemPrice.valid_upto,
+ )
+ .where(ItemPrice.item_code == item_code)
+ .where(ItemPrice.docstatus != 2)
+ .where((ItemPrice.valid_upto.isnull()) | (ItemPrice.valid_upto >= today))
+ .orderby(ItemPrice.price_list)
+ .limit(11)
+ .run(as_dict=True)
+ )
+
+ has_more = len(prices) == 11
+ return {
+ "prices": prices[:10],
+ "has_more": has_more,
+ }
diff --git a/erpnext/stock/doctype/item/item_prices.html b/erpnext/stock/doctype/item/item_prices.html
new file mode 100644
index 00000000000..277afaa4dea
--- /dev/null
+++ b/erpnext/stock/doctype/item/item_prices.html
@@ -0,0 +1,139 @@
+
+
+
+
+
+{% if (prices && prices.length) { %}
+
+
+
+
+
+ | {{ __("No.") }} |
+ {{ __("Price List") }} |
+ {{ __("Type") }} |
+ {{ __("Party") }} |
+ {{ __("Rate") }} |
+ {{ __("UOM") }} |
+ {{ __("Valid Upto") }} |
+
+
+
+ {% for (var i=0; i < prices.length; i++) { var p = prices[i]; %}
+
+ | {{ i + 1 }} |
+ {{ p.price_list }} |
+
+ {% if (p.buying && p.selling) { %}
+ {{ __("Buy & Sell") }}
+ {% } else if (p.buying) { %}
+ {{ __("Buying") }}
+ {% } else if (p.selling) { %}
+ {{ __("Selling") }}
+ {% } %}
+ |
+
+ {% if (p.customer) { %}
+ {{ p.customer }}
+ {% } else if (p.supplier) { %}
+ {{ p.supplier }}
+ {% } %}
+ |
+ {{ format_currency(p.price_list_rate, p.currency) }} |
+ {{ p.uom || stock_uom }} |
+ {{ p.valid_upto ? frappe.datetime.str_to_user(p.valid_upto) : "" }} |
+
+ {% } %}
+
+
+
+
+
+
+{% } else { %}
+
+
+
{{ __("No active item prices found.") }}
+
+
+
+{% } %}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js
index a5599e340a6..2faa3337689 100644
--- a/erpnext/stock/doctype/item_price/item_price.js
+++ b/erpnext/stock/doctype/item_price/item_price.js
@@ -10,6 +10,15 @@ frappe.ui.form.on("Item Price", {
},
};
});
+
+ frm._price_list_filters = {};
+ frm.set_query("price_list", () => ({ filters: frm._price_list_filters }));
+ },
+
+ refresh(frm) {
+ if (frm.doc.item_code) {
+ frm.trigger("item_code");
+ }
},
onload(frm) {
@@ -37,4 +46,20 @@ frappe.ui.form.on("Item Price", {
};
});
},
+
+ item_code(frm) {
+ frm._price_list_filters = {};
+ if (frm.doc.item_code) {
+ frappe.db
+ .get_value("Item", frm.doc.item_code, ["is_sales_item", "is_purchase_item"])
+ .then((r) => {
+ if (!r.message) return;
+ if (r.message.is_sales_item && !r.message.is_purchase_item) {
+ frm._price_list_filters.selling = 1;
+ } else if (r.message.is_purchase_item && !r.message.is_sales_item) {
+ frm._price_list_filters.buying = 1;
+ }
+ });
+ }
+ },
});