diff --git a/__manifest__.py b/__manifest__.py
index 6003ec2..f658d8d 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -48,6 +48,9 @@ Features
# ProductScreen portrait tab state + layout
'pos_ui_optimization/static/src/app/screens/product_screen/portrait_mode_patch.js',
'pos_ui_optimization/static/src/app/screens/product_screen/portrait_screen.xml',
+ # ProductCard and Orderline optimization patches
+ 'pos_ui_optimization/static/src/app/components/product_card/product_card_patch.xml',
+ 'pos_ui_optimization/static/src/app/components/orderline/orderline_patch.xml',
# Note splitting and defensive parsing patches
'pos_ui_optimization/static/src/app/screens/product_screen/notes_patch.js',
# Portrait CSS (must load after all XML patches)
diff --git a/models/pos_config.py b/models/pos_config.py
index 2f92e13..ea5a5c1 100644
--- a/models/pos_config.py
+++ b/models/pos_config.py
@@ -10,9 +10,15 @@ class PosConfig(models.Model):
help="Hide the Cash In/Out button in the POS Navbar menu"
)
+ hide_cart_images = fields.Boolean(
+ string="Hide Product Images in Cart",
+ default=False,
+ help="Hide product images in the order/cart lines inside POS"
+ )
+
@api.model
def _load_pos_data_fields(self, config):
fields_list = super()._load_pos_data_fields(config)
if fields_list:
- fields_list.append('hide_cash_in_out_button')
+ fields_list.extend(['hide_cash_in_out_button', 'hide_cart_images'])
return fields_list
diff --git a/models/res_config_settings.py b/models/res_config_settings.py
index 4c6ced7..5d2a62a 100644
--- a/models/res_config_settings.py
+++ b/models/res_config_settings.py
@@ -9,3 +9,9 @@ class ResConfigSettings(models.TransientModel):
related='pos_config_id.hide_cash_in_out_button',
readonly=False
)
+
+ pos_hide_cart_images = fields.Boolean(
+ string="Hide Product Images in Cart",
+ related='pos_config_id.hide_cart_images',
+ readonly=False
+ )
diff --git a/static/src/app/components/order_display/order_display_patch.js b/static/src/app/components/order_display/order_display_patch.js
index 321fff1..f7b00a6 100644
--- a/static/src/app/components/order_display/order_display_patch.js
+++ b/static/src/app/components/order_display/order_display_patch.js
@@ -37,15 +37,22 @@ patch(OrderDisplay.prototype, {
return;
}
+ let orderScrollTimeout = null;
const onScroll = () => {
- if (
- scrollable.scrollTop + scrollable.clientHeight >=
- scrollable.scrollHeight - 100
- ) {
- if (this.state.displayedCount < this.comboSortedLines.length) {
- this.state.displayedCount += 20;
- }
+ if (orderScrollTimeout) {
+ return;
}
+ orderScrollTimeout = setTimeout(() => {
+ orderScrollTimeout = null;
+ if (
+ scrollable.scrollTop + scrollable.clientHeight >=
+ scrollable.scrollHeight - 100
+ ) {
+ if (this.state.displayedCount < this.comboSortedLines.length) {
+ this.state.displayedCount += 20;
+ }
+ }
+ }, 150);
};
scrollable.addEventListener("scroll", onScroll);
diff --git a/static/src/app/components/orderline/orderline_patch.xml b/static/src/app/components/orderline/orderline_patch.xml
new file mode 100644
index 0000000..0c79d40
--- /dev/null
+++ b/static/src/app/components/orderline/orderline_patch.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ vals.productImage and !pos.config.hide_cart_images
+
+
+ lazy
+ async
+
+
+
diff --git a/static/src/app/components/product_card/product_card_patch.xml b/static/src/app/components/product_card/product_card_patch.xml
new file mode 100644
index 0000000..030b63f
--- /dev/null
+++ b/static/src/app/components/product_card/product_card_patch.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ lazy
+ async
+
+
+
diff --git a/static/src/app/screens/product_screen/notes_patch.js b/static/src/app/screens/product_screen/notes_patch.js
index 754ba6b..7e07b7b 100644
--- a/static/src/app/screens/product_screen/notes_patch.js
+++ b/static/src/app/screens/product_screen/notes_patch.js
@@ -3,6 +3,7 @@
import { NoteButton, InternalNoteButton } from "@point_of_sale/app/screens/product_screen/control_buttons/orderline_note_button/orderline_note_button";
import { Orderline } from "@point_of_sale/app/components/orderline/orderline";
import { patch } from "@web/core/utils/patch";
+import { usePos } from "@point_of_sale/app/hooks/pos_hook";
// 1. Patch NoteButton to fix line-splitting bug for customer notes in standard Odoo
patch(NoteButton.prototype, {
@@ -64,8 +65,13 @@ patch(InternalNoteButton.prototype, {
}
});
-// 3. Patch Orderline to prevent UI crashes if note contains invalid JSON
+// 3. Patch Orderline to prevent UI crashes if note contains invalid JSON and inject pos service
patch(Orderline.prototype, {
+ setup() {
+ super.setup();
+ this.pos = usePos();
+ },
+
get lineScreenValues() {
const originalParse = JSON.parse;
JSON.parse = function (str) {
diff --git a/static/src/app/screens/product_screen/product_screen_patch.js b/static/src/app/screens/product_screen/product_screen_patch.js
index acb1160..7e6284d 100644
--- a/static/src/app/screens/product_screen/product_screen_patch.js
+++ b/static/src/app/screens/product_screen/product_screen_patch.js
@@ -10,17 +10,24 @@ patch(ProductScreen.prototype, {
this.state.displayedProductsCount = 80;
const originalOnScroll = this.onScroll;
+ let scrollTimeout = null;
this.onScroll = (ev) => {
if (originalOnScroll) {
originalOnScroll(ev);
}
- const el = ev.target;
- if (el && el.scrollTop + el.clientHeight >= el.scrollHeight - 400) {
- const totalCount = this.pos.productToDisplayByCateg.reduce((acc, cat) => acc + cat[1].length, 0);
- if (this.state.displayedProductsCount < totalCount) {
- this.state.displayedProductsCount += 80;
- }
+ if (scrollTimeout) {
+ return;
}
+ scrollTimeout = setTimeout(() => {
+ scrollTimeout = null;
+ const el = ev.target;
+ if (el && el.scrollTop + el.clientHeight >= el.scrollHeight - 450) {
+ const totalCount = this.pos.productsToDisplay.length;
+ if (this.state.displayedProductsCount < totalCount) {
+ this.state.displayedProductsCount += 80;
+ }
+ }
+ }, 150);
};
useEffect(
diff --git a/static/src/app/services/pos_store_patch.js b/static/src/app/services/pos_store_patch.js
index c0404c0..2e82122 100644
--- a/static/src/app/services/pos_store_patch.js
+++ b/static/src/app/services/pos_store_patch.js
@@ -4,11 +4,31 @@ import { PosStore } from "@point_of_sale/app/services/pos_store";
import { patch } from "@web/core/utils/patch";
patch(PosStore.prototype, {
+ clearProductsCache() {
+ this._productsToDisplayCacheKey = null;
+ this._productsToDisplayCache = null;
+ this._productToDisplayByCategCacheKey = null;
+ this._productToDisplayByCategCache = null;
+ },
+
+ async editProduct(product) {
+ this.clearProductsCache();
+ return await super.editProduct(...arguments);
+ },
+
get productsToDisplay() {
+ const searchWord = this.searchProductWord.trim();
+ const categoryId = this.selectedCategory?.id || 0;
+ const templatesLength = this.models["product.template"].length;
+ const cacheKey = `${searchWord}_${categoryId}_${templatesLength}`;
+
+ if (this._productsToDisplayCacheKey === cacheKey && this._productsToDisplayCache) {
+ return this._productsToDisplayCache;
+ }
+
// We override this getter to remove Odoo's hardcoded 100-product limit.
// Because our custom UI optimization module uses progressive rendering (lazy loading)
// on scroll, we can safely return the entire product list without risking browser freezing.
- const searchWord = this.searchProductWord.trim();
let recordIterator;
const isSearchByWord = searchWord !== "";
@@ -56,26 +76,43 @@ patch(PosStore.prototype, {
filteredList.push(p);
}
+ let result = [];
if (
!isSearchByWord &&
!this.selectedCategory?.id &&
this.areAllProductsSpecial(filteredList)
) {
- return [];
+ result = [];
+ } else {
+ result = this.orderProductBySequenceAndFav(filteredList);
}
- return this.orderProductBySequenceAndFav(filteredList);
+ this._productsToDisplayCacheKey = cacheKey;
+ this._productsToDisplayCache = result;
+ return result;
},
get productToDisplayByCateg() {
+ const searchWord = this.searchProductWord.trim();
+ const categoryId = this.selectedCategory?.id || 0;
+ const templatesLength = this.models["product.template"].length;
+ const categoriesLength = this.models["pos.category"].length;
+ const cacheKey = `${searchWord}_${categoryId}_${templatesLength}_${categoriesLength}`;
+
+ if (this._productToDisplayByCategCacheKey === cacheKey && this._productToDisplayByCategCache) {
+ return this._productToDisplayByCategCache;
+ }
+
// We override this getter to remove Odoo's hardcoded 100-product category limit.
const sortedProducts = this.productsToDisplay;
if (!this.config.iface_group_by_categ) {
- return sortedProducts.length ? [["0", sortedProducts]] : [];
+ const result = sortedProducts.length ? [["0", sortedProducts]] : [];
+ this._productToDisplayByCategCacheKey = cacheKey;
+ this._productToDisplayByCategCache = result;
+ return result;
}
const results = [];
- const searchWord = this.searchProductWord.trim();
const byCateg = this.models["product.template"].toRaw().getAllBy("pos_categ_ids");
const selectedCategoryIds = !this.selectedCategory
? this.models["pos.category"].map((c) => c.id)
@@ -117,6 +154,8 @@ patch(PosStore.prototype, {
}
}
+ this._productToDisplayByCategCacheKey = cacheKey;
+ this._productToDisplayByCategCache = results;
return results;
}
});
diff --git a/static/src/scss/portrait.scss b/static/src/scss/portrait.scss
index fb0d81f..82b3741 100644
--- a/static/src/scss/portrait.scss
+++ b/static/src/scss/portrait.scss
@@ -415,3 +415,24 @@
}
}
}
+
+/* ============================================================
+ GENERAL POS PERFORMANCE OPTIMIZATIONS (All Display Modes)
+ ============================================================ */
+.pos .overflow-y-auto {
+ will-change: scroll-position;
+ -webkit-overflow-scrolling: touch;
+}
+
+.pos .product {
+ /* Disable transitions/animations on low-end ARM devices to make UI snappier */
+ transition: none !important;
+ animation: none !important;
+ transform: translateZ(0); /* GPU acceleration */
+ backface-visibility: hidden;
+}
+
+.pos .product-img img {
+ image-rendering: -webkit-optimize-contrast;
+ will-change: transform;
+}
diff --git a/views/res_config_settings_views.xml b/views/res_config_settings_views.xml
index eee794b..54d0c62 100644
--- a/views/res_config_settings_views.xml
+++ b/views/res_config_settings_views.xml
@@ -9,6 +9,9 @@
+
+
+