Advertisement
🖥️ POS Development

Odoo POS Models – Extending pos.order and Custom Models

Extending the POS on the Python side means adding fields to core models like pos.order, pos.order.line, and pos.config, controlling what data is sent to the browser, and writing methods that the JavaScript frontend calls via RPC.

⏱️ 22 min 🎯 Intermediate 📅 Updated 2026

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;
ConceptMeaning
Loaded modelsdata cached in the browser at POS startup
pos.order / orderlinethe client-side sale being built
Server synccompleted 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

  1. Load a backend field into POS via the data loader.
  2. Add a field to product.product for POS.
  3. Access it in the POS JS.
  4. Note that POS reads a snapshot.
  5. 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.

What you'll learn:
  • Adding fields to pos.order and pos.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:

Python
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:

Python
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:

Python
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_loyaltypos.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:

Python
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', ...).

Advertisement

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:

Python
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:

JavaScript
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():

Python
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
Key takeaways:
  • Extend pos.order / pos.order.line with fields, then override _order_fields() / _order_line_fields() to persist them
  • Override _get_pos_ui_data() on pos.config to include your model's data in the browser payload
  • Expose Python methods with @api.model for 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?

FAQ

How do I add a field to the printed receipt? +

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.

When should I use _get_pos_ui_data vs a real-time RPC call? +

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.

Can I add a custom table to POS (like a custom model, not pos.order)? +

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.