Advertisement
🖥️ POS Development

Odoo POS Payment Integration – Custom Payment Methods

Odoo POS supports cash and card payments out of the box. To integrate a custom payment terminal (e.g., a gift card system, a loyalty wallet, or a third-party payment provider), you implement a PaymentInterface subclass in JavaScript and back it with a pos.payment.method record in Python.

⏱️ 25 min 🎯 Advanced 📅 Updated 2026

POS Payments: Methods, Terminals, and Reconciliation

Payment is where POS meets money and accounting, so correctness matters. The POS supports multiple payment methods (cash, card, custom), can integrate hardware terminals, and must reconcile cleanly with Odoo's accounting.

ConceptRole
Payment methodcash, bank card, gift card, custom
Payment terminalhardware integration (Stripe, Adyen, etc.)
Journalthe accounting journal each method posts to
Session closingreconciles cash + card totals at end of shift

Payment methods are configured, then coded if custom: standard methods (cash, card) are set up in POS configuration and tied to accounting journals so every payment posts to the right account. A custom payment method (a new terminal, a loyalty/voucher scheme) means extending the POS JavaScript payment logic to handle its flow — authorize, confirm, handle failure/timeout, and record the amount. Terminal integration adds async complexity: a card terminal call is asynchronous and can be declined, cancelled, or time out — the POS code must handle each outcome gracefully so the cashier isn't stuck and the order state stays correct. Reconciliation is the accounting side: when a POS session (a cashier's shift) closes, Odoo tallies expected cash and card totals against what was collected; discrepancies are flagged. This is why payment amounts and methods must be recorded precisely — errors surface as unbalanced sessions. For developers: respect that payments are financial data — validate amounts, handle the async terminal states, ensure the payment ties to the right journal, and test the full close-session flow. Payment bugs aren't cosmetic; they affect the books.

🏋️ Practical Exercise

  1. Add a payment method to POS.
  2. Configure a payment terminal (concept).
  3. Handle a payment in JS.
  4. Validate an order.
  5. Handle the change due.

🔥 Challenge Exercise

Configure POS payment methods and walk through the payment flow: selecting a method, tendering an amount, computing change, and validating the order. Explain how custom payment methods integrate and where payment logic lives in the POS.

What you'll learn:
  • How pos.payment.method works
  • The PaymentInterface JavaScript class
  • Sending and receiving payment terminal messages
  • Handling payment success, failure, and cancellation
  • Split payment across multiple methods

pos.payment.method

pos.payment.method is the database model for a payment method. Each method is linked to a journal (bank or cash) in accounting. Fields relevant to custom integrations:

  • name — displayed on the PaymentScreen button
  • journal_id — linked accounting journal
  • use_payment_terminal — selection field; your module registers its terminal type here
  • pos_config_ids — which POS configs this method is available on

Register your terminal type by extending the use_payment_terminal selection field:

Python
from odoo import models, fields

class PosPaymentMethod(models.Model):
    _inherit = 'pos.payment.method'

    def _get_payment_terminal_selection(self):
        return super()._get_payment_terminal_selection() + [
            ('my_terminal', 'My Custom Terminal'),
        ]

PaymentInterface – JavaScript Class

Every payment terminal integration requires a JavaScript class that extends PaymentInterface. Register it so Odoo POS loads it when the payment method's use_payment_terminal matches your key:

JavaScript
/** @odoo-module **/
import { PaymentInterface } from '@point_of_sale/app/payment/payment_interface';
import { register_payment_method } from '@point_of_sale/app/store/pos_store';

export class PaymentMyTerminal extends PaymentInterface {

    /**
     * Called when the cashier clicks "Send" on the payment screen.
     * Start the terminal transaction here.
     */
    async sendPaymentRequest(cid) {
        const paymentLine = this.pos.get_order().get_paymentline(cid);
        const amount = paymentLine.get_amount();

        // Call Python method on the server to initiate the terminal
        const result = await this.env.services.orm.call(
            'pos.payment.method',
            'initiate_terminal_payment',
            [this.payment_method.id, amount],
        );

        if (result.success) {
            paymentLine.set_payment_status('waitingCard');
            return true;  // waiting for terminal callback
        }
        paymentLine.set_payment_status('retry');
        return false;
    }

    /**
     * Called when the cashier clicks "Cancel" during payment.
     */
    async sendPaymentCancel(order, cid) {
        await this.env.services.orm.call(
            'pos.payment.method',
            'cancel_terminal_payment',
            [this.payment_method.id],
        );
        return true;
    }
}

register_payment_method('my_terminal', PaymentMyTerminal);

Polling for Terminal Response

Payment terminals are asynchronous — the cashier sends a request and waits for a response. Poll the server for the terminal status:

JavaScript
export class PaymentMyTerminal extends PaymentInterface {

    async sendPaymentRequest(cid) {
        const paymentLine = this.pos.get_order().get_paymentline(cid);
        paymentLine.set_payment_status('waitingCard');

        // Poll every 2 seconds for terminal result
        this._pollingInterval = setInterval(async () => {
            const status = await this.env.services.orm.call(
                'pos.payment.method',
                'get_terminal_status',
                [this.payment_method.id],
            );

            if (status.state === 'approved') {
                clearInterval(this._pollingInterval);
                paymentLine.set_payment_status('done');
                paymentLine.transaction_id = status.transaction_id;
                this.pos.get_order().validate_order();
            } else if (status.state === 'declined') {
                clearInterval(this._pollingInterval);
                paymentLine.set_payment_status('retry');
            }
        }, 2000);

        return true;
    }
}
Advertisement

Split Payments

Odoo POS supports split payments natively — a customer can pay part in cash and part by card. The PaymentScreen shows multiple payment lines when the cashier adds multiple payment methods. No special code is needed; Odoo's validate_order() handles posting multiple payment lines to accounting.

Each payment line generates a separate pos.payment record and a corresponding journal entry. Your integration only needs to handle the payment lines assigned to your terminal.

To access the order's payment lines in JavaScript:

JavaScript
const order = this.pos.get_order();
const paymentLines = order.get_paymentlines();
const myLines = paymentLines.filter(
    line => line.payment_method.use_payment_terminal === 'my_terminal'
);
const totalMyTerminal = myLines.reduce(
    (sum, line) => sum + line.get_amount(), 0
);
Key takeaways:
  • Register your terminal type by extending _get_payment_terminal_selection() on pos.payment.method
  • Implement sendPaymentRequest() and sendPaymentCancel() in your PaymentInterface subclass
  • Register the class with register_payment_method('your_key', YourClass)
  • Split payments are handled automatically — no extra code needed

Interview Questions

  • How do payment methods work in POS?
  • How is change calculated?
  • How do you add a custom payment method?
  • What is a payment terminal integration?
  • Where does POS store payment lines?

FAQ

Does Odoo have built-in support for card payment terminals? +

Yes — Odoo has built-in integrations for Adyen, Stripe Terminal, and Ingenico via separate modules (pos_adyen, pos_stripe, etc.). These follow the same PaymentInterface pattern. If your terminal isn't supported natively, implement a custom integration using the pattern shown here.

How do I handle a terminal timeout? +

Set a maximum polling duration. If the terminal doesn't respond within (e.g.) 60 seconds, clear the interval, call sendPaymentCancel() to abort the terminal transaction, and set the payment line status to 'retry'. Always provide a way for the cashier to retry or cancel.

Where is pos.payment stored in accounting? +

When the session is closed, Odoo creates a bank/cash statement for each payment method with a journal. Each pos.payment becomes a line on the statement. Reconciliation against the journal's account happens when the session closes and calls action_pos_session_closing_control().