8.2 KiB
POS UI Optimization
Odoo 19 | Custom Addon
Optimizes the Point of Sale UI for resource-constrained devices and adds a switchable portrait display mode designed for 6-inch touchscreen terminals.
Features
1. Incremental Product Rendering
Prevents low-RAM devices from freezing when the product catalogue is large.
- Only 40 products are rendered on initial load
- More products load automatically as the user scrolls down
- Resets the counter when the search word or category changes
2. Incremental Order Line Rendering
Same approach for the order/cart display.
- Only 20 order lines rendered initially
- Scrolling loads more lines progressively
- Ensures the selected line is always visible (expands count if needed)
3. Hide Invoice Button
Hides the "Invoice" button on the payment stage (Payment Screen) and expands the Customer/Partner button to full width, preventing cashiers from using this button.
4. Portrait Mode (6-inch Display)
A dedicated compact layout for portrait-orientation small screens. Designed for devices like 6-inch Android POS terminals where the standard Odoo two-column layout clutters the screen.
Layout
┌──────────────────────────────────────┐
│ Navbar (compact) │
├──────────────────────────────────────┤
│ │
│ Active tab pane: │
│ Products / Cart / Numpad │
│ │
├──────────────────────────────────────┤
│ TOTAL: Rp X,XX [Checker] [Send] [Pay]│ ← always visible
├──────────────────────────────────────┤
│ [Products] [Cart] [Numpad] │ ← bottom tab bar
└──────────────────────────────────────┘
Key Differences from Standard Mode
| Feature | Standard | Portrait |
|---|---|---|
| Layout | Two-column (order left, products right) | Single pane with bottom tabs |
| Categories | Horizontal scrollable chips | Compact hierarchical select dropdown |
| Product grid | auto-fill minmax(115px) |
Fixed 2 columns |
| Product images | Full aspect ratio | Capped at 90px height |
| Numpad | Shown inline in left pane | Dedicated "Numpad" tab (shows only selected line) |
| Actions (Pay/Send/Checker) | Inside left pane action pad | Always-visible bottom strip |
| Navbar | Full labels | Compact with smaller padding |
Category Dropdown
In portrait mode, the category selector is transformed into a compact dropdown list. It lists all active POS categories formatted hierarchically with indentation, making it extremely easy to filter products on small touch screens.
Selected Line Actions (Note & Delete)
The Numpad tab contains actions for modifying the selected orderline:
- Note: Triggers the standard customer note input dialog.
- Delete: Instantly removes the selected line from the cart.
Numpad Single Line Focus
To prevent overlaps on 5.8-inch screens:
- Cart Tab: Shows only the order lines (full height). The numpad is hidden.
- Numpad Tab: Hides all non-selected lines and hides the order summary block, showing only the selected line and the numpad/actions. If no item is selected, a friendly guide card is displayed.
Table Checker Button
The Checker button is available in the bottom pay strip in portrait mode. It allows waiters or cashiers to quickly print a basic table checker receipt.
The button is visible only when:
- The
basic_receiptoption is enabled in POS settings. - The current cashier's role is either
waiterorcashier.
The button is disabled when the current order is empty.
Auto-Activation
Portrait mode auto-activates when the browser viewport width is < 400 px on first load. A manual override is always available via the burger menu (☰ → Switch to Portrait/Standard Mode). The choice is saved to localStorage.
Send Button Behavior
The Send button appears only when:
- The POS is in restaurant mode (
module_pos_restaurant = True) - At least one Preparation Category is configured in POS → Kitchen Printers settings
- The current order has unsent changes (
nbrOfChanges > 0) - The order is not a direct sale / refund
This mirrors Odoo's standard restaurant swapButton logic exactly.
5. Safe Release Table Guard (Multi-Device)
Prevents an accidental order cancellation when multiple POS devices share the same login account in a restaurant setup.
The Problem
When Device X places orders on a table, Device Y sees the table turn purple (occupied).
If Device Y opens that table before the order has fully synced locally, it may see an
empty order and display the Release table button. Clicking Release on Device Y
calls action_pos_order_cancel on the server, permanently cancelling the order
created on Device X.
The Fix
Before executing the Release action, this feature performs a quick server-side check:
-
An RPC call to
pos.order.check_table_has_real_orders(table_id)is made. -
If the server reports real orders with non-zero lines exist on this table:
- The release is blocked.
- An alert dialog informs the user: "This table has N active order(s) placed from another device."
- After the user acknowledges,
deviceSync.readDataFromServer()is triggered so the real orders from Device X appear on Device Y. - The user is sent back to the Floor Screen with the correct table state.
-
If the server confirms no real orders exist, the Release proceeds as normal.
Offline Fallback
If the RPC call fails (e.g. device is offline), the guard falls back to the original
unbookTable() behaviour to avoid blocking legitimate releases.
Installation
- Copy the
pos_ui_optimizationfolder into yourcustomaddonsdirectory - Restart the Odoo server
- Go to Apps → search for POS UI Optimization → Install
- Reload your POS session
Upgrade
./odoo-bin -u pos_ui_optimization -d <your_database>
Compatibility
| Item | Value |
|---|---|
| Odoo version | 19.0 |
| Depends on | point_of_sale |
| Compatible with | pos_restaurant, pos_custom_access, pos_kitchen_printer |
| License | LGPL-3 |
File Structure
pos_ui_optimization/
├── __manifest__.py
├── README.md
├── static/
│ └── src/
│ ├── app/
│ │ ├── components/
│ │ │ ├── category_selector/
│ │ │ │ ├── category_selector_patch.js # Hierarchical dropdown list
│ │ │ │ └── category_selector_patch.xml
│ │ │ ├── navbar/
│ │ │ │ ├── navbar_patch.js # Display Mode toggle in burger menu
│ │ │ │ └── navbar_patch.xml
│ │ │ └── order_display/
│ │ │ ├── order_display_patch.js # Incremental order line rendering
│ │ │ └── order_display_patch.xml
│ │ ├── screens/
│ │ │ ├── payment_screen/
│ │ │ │ └── payment_screen_patch.xml # Hide Invoice button patch
│ │ │ └── product_screen/
│ │ │ ├── order_summary/
│ │ │ │ └── safe_release_patch.js # Multi-device Release guard
│ │ │ ├── portrait_mode_patch.js # Tab state (products/cart/numpad)
│ │ │ ├── portrait_screen.xml # Portrait layout XML patch
│ │ │ ├── product_screen_patch.js # Incremental product rendering
│ │ │ └── product_screen_patch.xml
│ │ └── services/
│ │ └── portrait_mode.js # Auto-detect + localStorage service
│ └── scss/
│ └── portrait.scss # All portrait CSS (scoped to .pos-portrait)
Author
Suherdy Yacob