Merge pull request #55653 from frappe/mergify/bp/version-16-hotfix/pr-54853

feat: item prices list view (backport #54853)
This commit is contained in:
Khushi Rawat
2026-06-05 16:26:50 +05:30
committed by GitHub
5 changed files with 276 additions and 16 deletions

View File

@@ -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(
`<div class="text-muted text-center" style="padding: 20px;">${__("Loading...")}</div>`
);
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) {

View File

@@ -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": "<div id=\\\"item-prices-container\\\"></div>"
},
{
"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",

View File

@@ -1527,3 +1527,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,
}

View File

@@ -0,0 +1,139 @@
<!-- 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>
{% } %}

View File

@@ -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;
}
});
}
},
});