- How
pos.payment.methodworks - The
PaymentInterfaceJavaScript 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 buttonjournal_id— linked accounting journaluse_payment_terminal— selection field; your module registers its terminal type herepos_config_ids— which POS configs this method is available on
Register your terminal type by extending the use_payment_terminal selection field:
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:
/** @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:
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;
}
}
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:
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
);
- Register your terminal type by extending
_get_payment_terminal_selection()onpos.payment.method - Implement
sendPaymentRequest()andsendPaymentCancel()in yourPaymentInterfacesubclass - Register the class with
register_payment_method('your_key', YourClass) - Split payments are handled automatically — no extra code needed
Frequently Asked Questions
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().