mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-08 15:42:52 +00:00
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
This commit is contained in:
@@ -255,13 +255,14 @@ frappe.ui.form.on("Item", {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
erpnext.item.edit_prices_button(frm);
|
|
||||||
erpnext.item.toggle_attributes(frm);
|
erpnext.item.toggle_attributes(frm);
|
||||||
|
|
||||||
if (!frm.doc.is_fixed_asset) {
|
if (!frm.doc.is_fixed_asset) {
|
||||||
erpnext.item.make_dashboard(frm);
|
erpnext.item.make_dashboard(frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
erpnext.item.render_item_prices(frm);
|
||||||
|
|
||||||
frm.add_custom_button(__("Duplicate"), function () {
|
frm.add_custom_button(__("Duplicate"), function () {
|
||||||
var new_item = frappe.model.copy_doc(frm.doc);
|
var new_item = frappe.model.copy_doc(frm.doc);
|
||||||
// Duplicate item could have different name, causing "copy paste" error.
|
// Duplicate item could have different name, causing "copy paste" error.
|
||||||
@@ -665,24 +666,60 @@ $.extend(erpnext.item, {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
edit_prices_button: function (frm) {
|
render_item_prices: function (frm) {
|
||||||
frm.add_custom_button(
|
if (frm.doc.__islocal) return;
|
||||||
__("Add / Edit Prices"),
|
const requested_item = frm.doc.name;
|
||||||
function () {
|
const container = frm.fields_dict["prices_html"].$wrapper;
|
||||||
frappe.set_route("List", "Item Price", { item_code: frm.doc.name });
|
|
||||||
},
|
container.html(
|
||||||
__("Actions")
|
`<div class="text-muted text-center" style="padding: 20px;">${__("Loading...")}</div>`
|
||||||
);
|
);
|
||||||
|
|
||||||
frm.add_custom_button(
|
frappe.call({
|
||||||
__("Make Lead Time"),
|
method: "erpnext.stock.doctype.item.item.get_item_prices",
|
||||||
function () {
|
args: { item_code: requested_item },
|
||||||
frm.make_new("Item Lead Time", {
|
|
||||||
item_code: frm.doc.name,
|
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) {
|
weight_to_validate: function (frm) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"item_name",
|
"item_name",
|
||||||
"item_group",
|
"item_group",
|
||||||
"stock_uom",
|
"stock_uom",
|
||||||
|
"image",
|
||||||
"column_break0",
|
"column_break0",
|
||||||
"disabled",
|
"disabled",
|
||||||
"is_stock_item",
|
"is_stock_item",
|
||||||
@@ -39,7 +40,6 @@
|
|||||||
"over_delivery_receipt_allowance",
|
"over_delivery_receipt_allowance",
|
||||||
"column_break_wugd",
|
"column_break_wugd",
|
||||||
"over_billing_allowance",
|
"over_billing_allowance",
|
||||||
"image",
|
|
||||||
"section_break_11",
|
"section_break_11",
|
||||||
"brand",
|
"brand",
|
||||||
"description",
|
"description",
|
||||||
@@ -138,6 +138,9 @@
|
|||||||
"inspection_required_before_delivery",
|
"inspection_required_before_delivery",
|
||||||
"column_break_pxjh",
|
"column_break_pxjh",
|
||||||
"quality_inspection_template",
|
"quality_inspection_template",
|
||||||
|
"pricing_tab",
|
||||||
|
"item_prices_column",
|
||||||
|
"prices_html",
|
||||||
"dashboard_tab"
|
"dashboard_tab"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -256,6 +259,7 @@
|
|||||||
"description": "Used to create an opening Stock Entry with the Valuation Rate when the item is saved",
|
"description": "Used to create an opening Stock Entry with the Valuation Rate when the item is saved",
|
||||||
"fieldname": "opening_stock",
|
"fieldname": "opening_stock",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
|
"hidden": 1,
|
||||||
"label": "Opening Stock"
|
"label": "Opening Stock"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1064,6 +1068,23 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_kpmi",
|
"fieldname": "column_break_kpmi",
|
||||||
"fieldtype": "Column Break"
|
"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",
|
"icon": "fa fa-tag",
|
||||||
|
|||||||
@@ -1544,3 +1544,41 @@ def get_child_warehouses(warehouse):
|
|||||||
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||||
|
|
||||||
return get_child_warehouses(warehouse)
|
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,
|
||||||
|
}
|
||||||
|
|||||||
139
erpnext/stock/doctype/item/item_prices.html
Normal file
139
erpnext/stock/doctype/item/item_prices.html
Normal 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>
|
||||||
|
|
||||||
|
{% } %}
|
||||||
@@ -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) {
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user