mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-06 21:59:13 +00:00
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:
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -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