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.
| Concept | Role |
|---|---|
| Payment method | cash, bank card, gift card, custom |
| Payment terminal | hardware integration (Stripe, Adyen, etc.) |
| Journal | the accounting journal each method posts to |
| Session closing | reconciles 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
- Add a payment method to POS.
- Configure a payment terminal (concept).
- Handle a payment in JS.
- Validate an order.
- 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.
- 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
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?
Related Topics
FAQ
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.
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.
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().

