From 069f28feeb3df3c3278ed813968f2831b706b27f Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Tue, 6 Jan 2026 00:29:07 +0530 Subject: [PATCH 1/4] fix(pos): item selector section ui/ux --- erpnext/public/scss/point-of-sale.scss | 7 +++ .../page/point_of_sale/point_of_sale.py | 12 ++-- .../page/point_of_sale/pos_item_selector.js | 59 +++++++++++++------ 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index d86d2038adf..6eb6b56c570 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -117,6 +117,13 @@ overflow-y: scroll; overflow-x: hidden; + &.items-not-found { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + } + &.show-item-image { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--margin-lg); diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 50ab2404078..35d22e40fb7 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -122,11 +122,13 @@ def filter_result_items(result, pos_profile): @frappe.whitelist() -def get_parent_item_group(): - # Using get_all to ignore user permission - item_group = frappe.get_all("Item Group", {"lft": 1, "is_group": 1}, pluck="name") - if item_group: - return item_group[0] +def get_parent_item_group(pos_profile): + item_groups = get_item_groups(pos_profile) + + if not item_groups: + item_groups = frappe.get_all("Item Group", {"lft": 1, "is_group": 1}, pluck="name") + + return item_groups[0] if item_groups else None @frappe.whitelist() diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 835fd65f846..c40dfcfde5e 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -7,12 +7,14 @@ erpnext.PointOfSale.ItemSelector = class { this.events = events; this.pos_profile = pos_profile; this.hide_images = settings.hide_images; + this.item_display_class = this.hide_images ? "hide-item-image" : "show-item-image"; this.auto_add_item = settings.auto_add_item_to_cart; + this.get_parent_item_group(); this.inti_component(); } - inti_component() { + async inti_component() { this.prepare_dom(); this.make_search_bar(); this.load_items_data(); @@ -35,20 +37,20 @@ erpnext.PointOfSale.ItemSelector = class { this.$component = this.wrapper.find(".items-selector"); this.$items_container = this.$component.find(".items-container"); - const show_hide_images = this.hide_images ? "hide-item-image" : "show-item-image"; - this.$items_container.addClass(show_hide_images); + this.$items_container.addClass(this.item_display_class); + } + + async get_parent_item_group() { + const r = await frappe.call({ + method: "erpnext.selling.page.point_of_sale.point_of_sale.get_parent_item_group", + args: { + pos_profile: this.pos_profile, + }, + }); + if (r.message) this.item_group = this.parent_item_group = r.message; } async load_items_data() { - if (!this.item_group) { - frappe.call({ - method: "erpnext.selling.page.point_of_sale.point_of_sale.get_parent_item_group", - async: false, - callback: (r) => { - if (r.message) this.parent_item_group = r.message; - }, - }); - } if (!this.price_list) { const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); this.price_list = res.message.selling_price_list; @@ -64,8 +66,6 @@ erpnext.PointOfSale.ItemSelector = class { const price_list = (doc && doc.selling_price_list) || this.price_list; let { item_group, pos_profile } = this; - !item_group && (item_group = this.parent_item_group); - return frappe.call({ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items", freeze: true, @@ -76,16 +76,32 @@ erpnext.PointOfSale.ItemSelector = class { render_item_list(items) { this.$items_container.html(""); + if (!items?.length) { + this.set_items_not_found_banner(); + return; + } + + if (this.$items_container.hasClass("items-not-found")) { + this.$items_container.removeClass("items-not-found"); + this.$items_container.addClass(this.item_display_class); + } + if (this.hide_images) { this.$items_container.append(this.render_item_list_column_header()); } - items.forEach((item) => { + items?.forEach((item) => { const item_html = this.get_item_html(item); this.$items_container.append(item_html); }); } + set_items_not_found_banner() { + this.$items_container.removeClass(this.item_display_class); + this.$items_container.addClass("items-not-found"); + this.$items_container.html(__("Items not found.")); + } + render_item_list_column_header() { return `
Name
@@ -189,17 +205,18 @@ erpnext.PointOfSale.ItemSelector = class { fieldtype: "Link", options: "Item Group", placeholder: __("Select item group"), + only_select: true, onchange: function () { me.item_group = this.value; !me.item_group && (me.item_group = me.parent_item_group); me.filter_items(); + me.set_item_selector_filter_label(this.value); }, get_query: function () { - const doc = me.events.get_frm().doc; return { query: "erpnext.selling.page.point_of_sale.point_of_sale.item_group_query", filters: { - pos_profile: doc ? doc.pos_profile : "", + pos_profile: me.pos_profile, }, }; }, @@ -210,9 +227,17 @@ erpnext.PointOfSale.ItemSelector = class { this.search_field.toggle_label(false); this.item_group_field.toggle_label(false); + $(this.item_group_field.awesomplete.ul).css("min-width", "unset"); + this.attach_clear_btn(); } + set_item_selector_filter_label(value) { + const $filter_label = this.$component.find(".label"); + + $filter_label.html(value ? __(value) : __("All Items")); + } + attach_clear_btn() { this.search_field.$wrapper.find(".control-input").append( ` From aef2e2794ba62c93680c7fb127af505e50888455 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Thu, 8 Jan 2026 18:15:03 +0530 Subject: [PATCH 2/4] fix: race condition --- erpnext/selling/page/point_of_sale/pos_item_selector.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index c40dfcfde5e..b6b92c2204e 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -10,11 +10,11 @@ erpnext.PointOfSale.ItemSelector = class { this.item_display_class = this.hide_images ? "hide-item-image" : "show-item-image"; this.auto_add_item = settings.auto_add_item_to_cart; - this.get_parent_item_group(); + this.item_ready_group = this.get_parent_item_group(); this.inti_component(); } - async inti_component() { + inti_component() { this.prepare_dom(); this.make_search_bar(); this.load_items_data(); @@ -51,6 +51,8 @@ erpnext.PointOfSale.ItemSelector = class { } async load_items_data() { + await this.item_ready_group; + if (!this.price_list) { const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); this.price_list = res.message.selling_price_list; From 02cefa8bdbdab40430f7bf81498a6756d73ed7e4 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Thu, 8 Jan 2026 18:54:14 +0530 Subject: [PATCH 3/4] fix: item group field clear button --- .../page/point_of_sale/pos_item_selector.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index b6b92c2204e..d03b2a0b8fa 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -231,6 +231,7 @@ erpnext.PointOfSale.ItemSelector = class { $(this.item_group_field.awesomplete.ul).css("min-width", "unset"); + this.hide_open_link_btn(); this.attach_clear_btn(); } @@ -240,6 +241,10 @@ erpnext.PointOfSale.ItemSelector = class { $filter_label.html(value ? __(value) : __("All Items")); } + hide_open_link_btn() { + $(this.item_group_field.$wrapper.find(".btn-open")).css("display", "none"); + } + attach_clear_btn() { this.search_field.$wrapper.find(".control-input").append( ` @@ -249,12 +254,24 @@ erpnext.PointOfSale.ItemSelector = class { ` ); + this.item_group_field.$wrapper.find(".link-btn").append( + ` + ${frappe.utils.icon("close", "xs", "es-icon")} + ` + ); + this.$clear_search_btn = this.search_field.$wrapper.find(".link-btn"); + this.$clear_item_group_btn = this.item_group_field.$wrapper.find(".btn-clear"); this.$clear_search_btn.on("click", "a", () => { this.set_search_value(""); this.search_field.set_focus(); }); + + this.$clear_item_group_btn.on("click", () => { + $(this.item_group_field.$input[0]).val("").trigger("input"); + this.item_group_field.set_focus(); + }); } set_search_value(value) { From 4d8d29b0df14cd2abeed1cdbf1cc0d5b7f2350f5 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Thu, 8 Jan 2026 21:35:55 +0530 Subject: [PATCH 4/4] fix: animate on item load --- erpnext/public/scss/point-of-sale.scss | 29 +++++++++++ .../page/point_of_sale/pos_item_selector.js | 52 +++++++++++++------ 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index 6eb6b56c570..5ef6e8fc5db 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -117,6 +117,35 @@ overflow-y: scroll; overflow-x: hidden; + &.item-loading { + position: relative; + pointer-events: none; + } + + &.item-loading::after { + content: ""; + position: absolute; + inset: 0; + background: repeating-linear-gradient( + 90deg, + #f3f3f3 0px, + #f3f3f3 160px, + #e9ecef 160px, + #e9ecef 320px + ); + animation: skeletonMove 1.1s linear infinite; + z-index: 1; + } + + @keyframes skeletonMove { + from { + background-position: 0 0; + } + to { + background-position: 320px 0; + } + } + &.items-not-found { display: flex; align-items: center; diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index d03b2a0b8fa..69ec1e56934 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -53,14 +53,20 @@ erpnext.PointOfSale.ItemSelector = class { async load_items_data() { await this.item_ready_group; + this.start_item_loading_animation(); + if (!this.price_list) { const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); this.price_list = res.message.selling_price_list; } - this.get_items({}).then(({ message }) => { - this.render_item_list(message.items); - }); + this.get_items({}) + .then(({ message }) => { + this.render_item_list(message.items); + }) + .always(() => { + this.stop_item_loading_animation(); + }); } get_items({ start = 0, page_length = 40, search_term = "" }) { @@ -403,6 +409,8 @@ erpnext.PointOfSale.ItemSelector = class { } filter_items({ search_term = "" } = {}) { + this.start_item_loading_animation(); + const selling_price_list = this.events.get_frm().doc.selling_price_list; if (search_term) { @@ -423,19 +431,31 @@ erpnext.PointOfSale.ItemSelector = class { } } - this.get_items({ search_term }).then(({ message }) => { - // eslint-disable-next-line no-unused-vars - const { items, serial_no, batch_no, barcode } = message; - if (search_term && !barcode) { - this.search_index[selling_price_list][search_term] = items; - } - this.items = items; - this.render_item_list(items); - this.auto_add_item && - this.search_field.$input[0].value && - this.items.length == 1 && - this.add_filtered_item_to_cart(); - }); + this.get_items({ search_term }) + .then(({ message }) => { + // eslint-disable-next-line no-unused-vars + const { items, serial_no, batch_no, barcode } = message; + if (search_term && !barcode) { + this.search_index[selling_price_list][search_term] = items; + } + this.items = items; + this.render_item_list(items); + this.auto_add_item && + this.search_field.$input[0].value && + this.items.length == 1 && + this.add_filtered_item_to_cart(); + }) + .always(() => { + this.stop_item_loading_animation(); + }); + } + + start_item_loading_animation() { + this.$items_container.addClass("is-loading"); + } + + stop_item_loading_animation() { + this.$items_container.removeClass("is-loading"); } add_filtered_item_to_cart() {