Ad – 728×90
🖥️ 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
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', ...).

Ad – 728×90

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

Frequently Asked Questions

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.