POS Data Models: Loaded to the Browser
The Point of Sale is offline-first: it preloads the data it needs (products, prices, taxes, customers) into the browser at startup so sales work without a server round-trip. The consequence for developers: adding a field the POS uses means telling it to load that field.
// tell the POS to load an extra field on product.product
import { registry } from "@web/core/registry";
// in POS, models declare which fields load into the browser cache
// (via the POS store / loaded models config)
// then access it client-side:
this.pos.db.get_product_by_id(id).x_my_field;
| Concept | Meaning |
|---|---|
| Loaded models | data cached in the browser at POS startup |
| pos.order / orderline | the client-side sale being built |
| Server sync | completed orders pushed back to Odoo |
The offline-first consequence: unlike normal Odoo screens (which query the server on demand), the POS can't do that mid-sale — the network might be down. So it loads a defined set of models and fields into the browser up front. If you add a custom field to product.product and want it available in the POS, you must extend the POS's list of loaded fields — otherwise the browser simply doesn't have it, and it reads as undefined. This is the #1 POS customization gotcha. Two model worlds: there are the normal Python/Postgres models and their JavaScript representations in the POS (the cart, order, orderlines built client-side). Logic that must run during a sale (custom pricing, discounts) has to be implemented in JavaScript on the POS side, not only in Python. Sync: when a sale completes, the POS creates the real pos.order records on the server (queuing them if offline, syncing when reconnected). Takeaway: POS development is front-end-heavy and revolves around "what data is loaded into the browser, and where does logic run" — a different mindset from backend Odoo work.
🏋️ Practical Exercise
- Load a backend field into POS via the data loader.
- Add a field to
product.productfor POS. - Access it in the POS JS.
- Note that POS reads a snapshot.
- Reflect a change after reloading the session.
🔥 Challenge Exercise
Expose an extra backend field to the POS frontend by adding it to the POS data loader, then read it in the POS JS models. Explain how POS loads a snapshot of backend data at session start and how to add fields to that snapshot.
- Adding fields to
pos.orderandpos.order.line - Overriding
_get_pos_ui_data()to send a custom model's data - Writing
_order_fields()to persist custom fields from the frontend - Exposing Python methods as RPC endpoints for the POS frontend
Extending pos.order
To add a custom field to a POS order (e.g., a table number or membership ID), extend pos.order:
from odoo import models, fields
class PosOrder(models.Model):
_inherit = 'pos.order'
membership_card = fields.Char(string='Membership Card', index=True)
loyalty_points_earned = fields.Integer(string='Loyalty Points Earned')
Adding the field to the model is not enough — the field also needs to be persisted when the order is created from the UI (see _order_fields below).
Saving Custom Fields: _order_fields
When the POS frontend submits an order to /pos/create_from_ui, Odoo calls _order_fields() to extract which fields from the JSON payload to write to the database. Override it to include your custom fields:
class PosOrder(models.Model):
_inherit = 'pos.order'
membership_card = fields.Char(string='Membership Card')
@api.model
def _order_fields(self, ui_order):
fields = super()._order_fields(ui_order)
fields['membership_card'] = ui_order.get('membership_card')
return fields
The ui_order dict is the raw JSON sent by the browser. Extract your field from it and add it to the fields dict returned by super().
Extending pos.config
Configuration fields that appear in POS settings are added to pos.config:
from odoo import models, fields
class PosConfig(models.Model):
_inherit = 'pos.config'
enable_loyalty = fields.Boolean(
string='Enable Loyalty Program',
default=False,
)
loyalty_points_per_currency = fields.Float(
string='Points per Currency Unit',
default=1.0,
)
These fields are accessible in the frontend as this.env.pos.config.enable_loyalty — pos.config is always loaded in the POS data payload.
Sending Custom Model Data: _get_pos_ui_data
Override _get_pos_ui_data() on pos.config to include a custom model in the data sent to the browser at session open:
class PosConfig(models.Model):
_inherit = 'pos.config'
def _get_pos_ui_data(self, params):
data = super()._get_pos_ui_data(params)
# Add loyalty cards to the initial data
data['loyalty.card'] = self.env['loyalty.card'].search_read(
[('company_id', '=', self.company_id.id)],
fields=['id', 'partner_id', 'points', 'code'],
)
return data
The key in the returned dict ('loyalty.card') is what the JavaScript frontend uses to access the data as this.env.pos.db.getBy('loyalty.card', ...).
Exposing Python Methods via RPC
For operations that must hit the server in real time (validating a card, checking external inventory), expose a Python method that the JS frontend can call:
from odoo import models, api
class PosOrder(models.Model):
_inherit = 'pos.order'
@api.model
def validate_loyalty_card(self, card_code):
"""Called from POS frontend to validate a loyalty card."""
card = self.env['loyalty.card'].search([
('code', '=', card_code),
], limit=1)
if not card:
return {'valid': False, 'message': 'Card not found.'}
return {
'valid': True,
'points': card.points,
'partner_name': card.partner_id.name,
}
From JavaScript, call this method using the ORM service:
const result = await this.env.services.orm.call(
'pos.order',
'validate_loyalty_card',
[cardCode],
);
if (result.valid) {
console.log(`Card valid — ${result.points} points`);
}
Extending pos.order.line
Custom fields on order lines follow the same pattern — extend pos.order.line and override _order_line_fields():
class PosOrderLine(models.Model):
_inherit = 'pos.order.line'
is_promotional = fields.Boolean(string='Promotional Item')
@api.model
def _order_line_fields(self, line, session_id=None):
fields = super()._order_line_fields(line, session_id)
fields[1]['is_promotional'] = line[1].get('is_promotional', False)
return fields
- Extend
pos.order/pos.order.linewith fields, then override_order_fields()/_order_line_fields()to persist them - Override
_get_pos_ui_data()onpos.configto include your model's data in the browser payload - Expose Python methods with
@api.modelfor real-time server calls from the JS frontend
Interview Questions
- How does POS get its backend data?
- How do you add a field to the POS models?
- When is POS data loaded?
- Why doesn’t POS see live backend changes mid-session?
- What is the POS models loader?
Related Topics
FAQ
Extend the receipt QWeb template in XML: inherit point_of_sale.receipt and use XPath to add your field. The field must be on pos.order and included in the data sent to the frontend so the receipt template can access it.
Use _get_pos_ui_data for data the POS needs at startup and that doesn't change often: loyalty cards, membership tiers, store-specific price rules. Use real-time RPC for data that changes during the session or requires server-side validation: payment terminal responses, stock checks, dynamic discounts.
Yes — include your model's data in _get_pos_ui_data() and register it in the JavaScript side. The POS has a client-side "database" (PosDB) that indexes models by ID and allows fast lookups. Register your model there to make queries from the frontend fast.

