refactor: use EmbeddedList for item prices

Replaces the hand-rolled item_prices.html table with frappe.ui.EmbeddedList,
the same primitive the proforma list uses. Drops the custom markup and styles.

The 10-row cap and the "View All Prices" link stay: the query fetches 11 rows
to return 10 plus a has_more flag, and the link routes to the Item Price list
filtered by item.
This commit is contained in:
khushi8112
2026-08-12 17:38:28 +05:30
parent 43de54b907
commit 51cefeffef
3 changed files with 103 additions and 190 deletions

View File

@@ -856,57 +856,107 @@ $.extend(erpnext.item, {
}
frm.toggle_display("prices_html", true);
const requested_item = frm.doc.name;
const container = frm.fields_dict["prices_html"].$wrapper;
frappe.require("embedded_list.bundle.js", () => erpnext.item.build_prices_list(frm));
},
container.html(
`<div class="text-muted text-center" style="padding: 20px;">${__("Loading...")}</div>`
);
build_prices_list: function (frm) {
const item_code = frm.doc.name;
const container = frm.fields_dict["prices_html"].$wrapper.empty();
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"));
const list = new frappe.ui.EmbeddedList({
wrapper: $("<div></div>").appendTo(container),
description: __("All active prices for this item across buying and selling price lists."),
show_index: true,
show_search: false,
empty_icon: "tag",
empty_message: __("No active item prices found."),
add_button: {
label: __("Add Price"),
action: () => erpnext.item.new_item_price(frm),
},
on_row_click: (row) => frappe.set_route("Form", "Item Price", row.name),
get_data() {
return frappe
.xcall("erpnext.stock.doctype.item.item.get_item_prices", { item_code })
.then((r) => {
this._has_more = r.has_more;
return r.prices;
});
},
before_render() {
this._all_data.forEach((row) => {
row.price_type =
row.buying && row.selling
? __("Buy & Sell")
: row.buying
? __("Buying")
: __("Selling");
});
},
columns: [
{ label: __("Price List"), fieldname: "price_list" },
{
label: __("Type"),
type: "badge",
fieldname: "price_type",
},
{
label: __("Party"),
type: "link",
text: (row) => row.customer || row.supplier || "",
route: (row) => [
"Form",
row.customer ? "Customer" : "Supplier",
row.customer || row.supplier,
],
},
{
label: __("Rate"),
fieldname: "price_list_rate",
render: (row) => format_currency(row.price_list_rate, row.currency),
},
{
label: __("UOM"),
fieldname: "uom",
render: (row) => frappe.utils.escape_html(row.uom || frm.doc.stock_uom || ""),
},
{
label: __("Valid Upto"),
fieldname: "valid_upto",
render: (row) => (row.valid_upto ? frappe.datetime.str_to_user(row.valid_upto) : ""),
},
],
});
list.refresh().then(() => {
if (!list._has_more) return;
frappe.ui
.button({
label: __("View All Prices"),
variant: "subtle",
size: "sm",
onclick: () => {
frappe.route_options = { item_code };
frappe.set_route("List", "Item Price");
},
})
.appendTo(
$('<div class="flex justify-end" style="margin-bottom: 8px;"></div>').appendTo(container)
);
});
},
new_item_price: function (frm) {
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: frm.doc.name, uom: frm.doc.stock_uom }, (dialog) => {
if (Object.keys(filters).length) {
dialog.fields_dict.price_list.get_query = () => ({ filters });
}
});
},

View File

@@ -1594,6 +1594,9 @@ def get_child_warehouses(warehouse):
return get_child_warehouses(warehouse)
ITEM_PRICES_LIMIT = 10
@frappe.whitelist()
def get_item_prices(item_code: str):
"""Fetch valid item prices for the item prices tab."""
@@ -1621,14 +1624,13 @@ def get_item_prices(item_code: str):
.where(ItemPrice.docstatus != 2)
.where((ItemPrice.valid_upto.isnull()) | (ItemPrice.valid_upto >= today))
.orderby(ItemPrice.price_list)
.limit(11)
.limit(ITEM_PRICES_LIMIT + 1)
.run(as_dict=True)
)
has_more = len(prices) == 11
return {
"prices": prices[:10],
"has_more": has_more,
"prices": prices[:ITEM_PRICES_LIMIT],
"has_more": len(prices) > ITEM_PRICES_LIMIT,
}

View File

@@ -1,139 +0,0 @@
<!-- Item Prices Tab -->
<style>
.item-price-table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-md);
color: var(--text-color);
}
.item-price-table thead tr {
background: var(--subtle-fg);
}
.item-price-table th {
padding: 8px 10px;
font-weight: 400;
color: var(--text-muted);
border-bottom: 1px solid var(--border-color);
white-space: nowrap;
}
.item-price-table td {
padding: 9px 10px;
border-bottom: 1px solid var(--border-color);
}
.item-price-table tbody tr:last-child td {
border-bottom: none;
}
.item-price-table tbody tr.price-row {
cursor: pointer;
}
.item-price-table tbody tr.price-row:hover {
background: var(--fg-hover-color);
}
.item-price-table .col-no {
width: 42px;
text-align: center;
color: var(--text-muted);
font-size: var(--text-sm);
}
.item-price-table .col-rate {
text-align: right;
font-variant-numeric: tabular-nums;
}
.item-price-table th.col-rate {
text-align: right;
}
.item-price-table .col-muted {
color: var(--text-muted);
}
.item-price-table a {
color: var(--text-color);
}
.item-prices-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
margin-bottom: 16px;
}
</style>
<div style="margin-bottom: 12px;">
<div class="text-extra-muted" style="font-size: var(--text-sm);">{{ __("All active prices for this item across buying and selling price lists.") }}</div>
</div>
{% if (prices && prices.length) { %}
<div style="border: 1px solid var(--border-color); border-radius: var(--border-radius-md); overflow: hidden; margin-bottom: 16px;">
<table class="item-price-table">
<thead>
<tr>
<th class="col-no">{{ __("No.") }}</th>
<th>{{ __("Price List") }}</th>
<th>{{ __("Type") }}</th>
<th>{{ __("Party") }}</th>
<th class="col-rate">{{ __("Rate") }}</th>
<th>{{ __("UOM") }}</th>
<th>{{ __("Valid Upto") }}</th>
</tr>
</thead>
<tbody>
{% for (var i=0; i < prices.length; i++) { var p = prices[i]; %}
<tr class="price-row" data-name="{{ p.name }}">
<td class="col-no">{{ i + 1 }}</td>
<td>{{ p.price_list }}</td>
<td>
{% if (p.buying && p.selling) { %}
{{ __("Buy & Sell") }}
{% } else if (p.buying) { %}
{{ __("Buying") }}
{% } else if (p.selling) { %}
{{ __("Selling") }}
{% } %}
</td>
<td>
{% if (p.customer) { %}
<a href="/app/customer/{{ encodeURIComponent(p.customer) }}" onclick="event.stopPropagation()">{{ p.customer }}</a>
{% } else if (p.supplier) { %}
<a href="/app/supplier/{{ encodeURIComponent(p.supplier) }}" onclick="event.stopPropagation()">{{ p.supplier }}</a>
{% } %}
</td>
<td class="col-rate">{{ format_currency(p.price_list_rate, p.currency) }}</td>
<td class="col-muted">{{ p.uom || stock_uom }}</td>
<td class="col-muted">{{ p.valid_upto ? frappe.datetime.str_to_user(p.valid_upto) : "" }}</td>
</tr>
{% } %}
</tbody>
</table>
</div>
<div class="item-prices-footer">
<div>
{% if (has_more) { %}
<a href="/app/item-price?item_code={{ encodeURIComponent(item_code) }}" class="btn btn-xs btn-default">
{{ __("View All Prices") }}
</a>
{% } %}
</div>
<button class="btn btn-xs btn-default add-price-btn">
{{ __("+ Add Price") }}
</button>
</div>
{% } else { %}
<div style="text-align: center; padding: 40px 20px; color: var(--text-muted); border: 1px dashed var(--border-color); border-radius: var(--border-radius-md); margin-bottom: 14px;">
<p style="margin-bottom: 12px;">{{ __("No active item prices found.") }}</p>
<button class="btn btn-sm btn-default add-price-btn">{{ __("+ Add Price") }}</button>
</div>
{% } %}