Odoo POS: Custom receipts and payments
Published
This guide walks through two practical tweaks I apply on most POS roll‑outs, giving you a quick, repeatable pattern to brand receipts and cut clicks at checkout without touching core code or fighting upgrades:
1) Brand the printed ticket and inject a QR code on the receipt. 2) Add a new payment method and auto‑select the right one (cash vs. card) on the Payment screen.
Everything below is a thin addon you can drop into your instance. No core patching.
Section headers: Receipt, Payments, Putting it together.
Receipt — extend the ticket and add a QR
- Goal: show a short “thank you” note and a QR that points to an order page (or a loyalty URL).
- Strategy: patch the data that feeds the receipt and minimally extend the receipt template.
Addon layout
mt_pos_custom/
__manifest__.py
static/src/js/receipt_data.js
static/src/xml/receipt.xml
static/src/js/payment_default.js
data/payment_method.xml # optional, for creating a payment method
manifest
# mt_pos_custom/__manifest__.py
{
"name": "POS – receipt + payment tweaks",
"version": "1.0",
"depends": ["point_of_sale", "account"],
"assets": {
# Load in the POS web client
"point_of_sale._assets_pos": {
"mt_pos_custom/static/src/js/receipt_data.js",
"mt_pos_custom/static/src/xml/receipt.xml",
"mt_pos_custom/static/src/js/payment_default.js",
},
},
}
Inject fields into the receipt data
// mt_pos_custom/static/src/js/receipt_data.js
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { Order } from "@point_of_sale/app/store/models";
import { qrCodeSrc } from "@point_of_sale/utils";
const superExport = Order.prototype.export_for_printing;
patch(Order.prototype, "mt_pos_custom.receipt_data", {
export_for_printing() {
const data = superExport.call(this);
// Example URL – adapt to your use‑case (loyalty, invoice request, survey, etc.)
const target = `${this.pos.base_url}/shop/order/${encodeURIComponent(this.name)}`;
data.custom_qr = qrCodeSrc(target);
data.custom_note = `Thanks for your purchase — ${this.name}`;
return data;
},
});
Extend the receipt template
<!-- mt_pos_custom/static/src/xml/receipt.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!-- Insert a note + QR above the footer on the receipt -->
<t t-extend="point_of_sale.OrderReceipt">
<t t-jquery=".before-footer" t-operation="before">
<div class="pos-receipt-center-align">
<t t-if="props.data.custom_qr">
<img t-att-src="props.data.custom_qr" style="width:96px;height:96px;margin:6px auto;"/>
</t>
<div t-if="props.data.custom_note" style="margin-top:4px">
<t t-esc="props.data.custom_note"/>
</div>
</div>
</t>
</t>
</templates>
Why this works
- The receipt component receives a
data
object built from the current order. We patch the order’sexport_for_printing()
to add two fields (custom_qr
,custom_note
). - The XML extension drops a small block before the footer using Odoo’s template extension mechanism. No core files are edited.
Payments — add a method and auto‑select one on the Payment screen
Add (or map) a payment method You can do this in the UI (Settings → Point of Sale → Payment Methods) or with XML. Here’s a minimal example that creates a bank method and reuses an existing Bank journal:
<!-- mt_pos_custom/data/payment_method.xml -->
<odoo>
<record id="pm_pos_card" model="pos.payment.method">
<field name="name">Card</field>
<!-- Map to a bank journal; replace with your journal external id -->
<field name="journal_id" ref="account.bank_journal"/>
</record>
</odoo>
- After installing the addon, open your POS Configuration and add the method under “Payments”.
- If you need a dedicated journal, create an
account.journal
of type Bank and reference it in the XML instead ofaccount.bank_journal
.
Auto‑select the right method at checkout The Payment screen is an OWL component. We can patch its lifecycle and add a payment line automatically based on the order context.
// mt_pos_custom/static/src/js/payment_default.js
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen";
const superOnMounted = PaymentScreen.prototype.onMounted;
patch(PaymentScreen.prototype, "mt_pos_custom.payment_default", {
onMounted() {
if (superOnMounted) superOnMounted.call(this);
try {
const order = this.currentOrder;
const due = order.get_due() || order.get_total_with_tax();
const hasLines = this.paymentLines.length > 0;
if (hasLines || due <= 0) return;
// Simple rule: small tickets use cash; otherwise use card/bank.
const threshold = 50; // change to your needs
const cash = this.payment_methods_from_config.find((m) => m.type === "cash");
const card = this.payment_methods_from_config.find((m) => m.type === "bank" || /card/i.test(m.name));
const target = due < threshold && cash ? cash : card || cash || this.payment_methods_from_config[0];
if (target) {
this.addNewPaymentLine(target);
}
} catch (err) {
// Never block payment UI
console.warn("mt_pos_custom: default payment selection failed", err);
}
},
});
Why this works
payment_methods_from_config
already contains only the methods enabled on the POS.- Each method has a computed
type
(cash
,bank
, orpay_later
), so you don’t need to inspect journals on the client. - We only add a line if there isn’t one yet.
Putting it together 1) Drop the addon in your addons path and update apps; install it on the database. 2) In the POS configuration, add the new payment method under Payments (if you created one). 3) Open the POS, create an order, pay — you should see a payment line pre‑selected. Print the ticket to see the note and QR.
Troubleshooting
- “Nothing changes on the receipt”: ensure your XML is loaded into
point_of_sale._assets_pos
and clear the browser cache; re‑enter the POS. - “The QR is broken”: if you’re fully offline, prefer generating with
qrCodeSrc
as above; avoid pointing to a server‑only barcode endpoint. - “Auto‑select doesn’t run”: check the browser console; another module may also patch
PaymentScreen
. You can rename your patch key (second param topatch
) to avoid collisions.
That’s it — a clean way to brand the ticket and remove clicks at payment time.