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 @@ + + +
+
{{ __("All active prices for this item across buying and selling price lists.") }}
+
+{% if (prices && prices.length) { %} + +
+ + + + + + + + + + + + + + {% for (var i=0; i < prices.length; i++) { var p = prices[i]; %} + + + + + + + + + + {% } %} + +
{{ __("No.") }}{{ __("Price List") }}{{ __("Type") }}{{ __("Party") }}{{ __("Rate") }}{{ __("UOM") }}{{ __("Valid Upto") }}
{{ 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; + } + }); + } + }, });