Skip to Content

Odoo

QPay V2 payment provider module for Odoo 17+. Extends payment.provider and payment.transaction models to integrate QPay into Odoo’s payment flow with QR code display, bank app deep links, webhook handling, and automatic transaction confirmation.


Requirements

RequirementVersion
Odoo17.0+
Python3.10+
requests libraryRequired (included in Odoo)

The module depends on the payment Odoo core module.


Installation

Standard Installation

  1. Download or clone the repository:
    git clone https://github.com/qpay-sdk/qpay-odoo.git
  2. Copy the payment_qpay/ directory to your Odoo addons path (e.g., /opt/odoo/addons/ or your custom addons directory)
  3. Restart the Odoo server
  4. Go to Settings > Apps > Update Apps List and click Update
  5. Search for QPay Payment Provider in the Apps list and click Install

Docker Installation

If using Docker, mount the payment_qpay/ directory into the addons volume:

volumes: - ./payment_qpay:/mnt/extra-addons/payment_qpay

Then update the apps list and install from the Odoo web interface.


Configuration

Navigate to Invoicing > Configuration > Payment Providers (or Accounting > Configuration > Payment Providers) and select QPay.

SettingDescriptionDefault
QPay Base URLQPay API endpointhttps://merchant.qpay.mn
QPay UsernameQPay merchant username
QPay PasswordQPay merchant password
QPay Invoice CodeQPay merchant invoice code
StateProvider state (disabled, enabled, test)Disabled

The username and password fields are restricted to the base.group_system security group, so only system administrators can view and modify them.

After configuring the credentials, set the provider state to Enabled and publish it to make QPay available at checkout.

For sandbox testing, set the QPay Base URL to:

https://merchant-sandbox.qpay.mn

How It Works

Payment Flow

  1. Checkout: Customer selects QPay as the payment provider on the checkout page or payment form
  2. Transaction Creation: Odoo creates a payment.transaction record and calls _get_specific_rendering_values() on the QPay transaction model
  3. Invoice Creation: The transaction model calls POST /v2/invoice via _qpay_make_request() on the provider model, passing the order reference, amount, customer email, and callback URL
  4. QR Display: The payment template (payment_qpay_templates.xml) renders:
    • A QR code image (base64-encoded PNG)
    • A “Pay with QPay” short URL button
    • Bank app deep links for supported Mongolian banks
    • A “Waiting for payment…” spinner
  5. Client-side Polling: JavaScript polls /payment/qpay/check every 3 seconds for up to 5 minutes. On success, the page reloads automatically
  6. Webhook Confirmation: QPay sends a POST to /payment/qpay/webhook. The controller finds the matching transaction and calls _handle_notification_data(), which triggers _process_notification_data() to verify the payment and set the transaction to done

Currency Filtering

The module automatically filters itself from available providers when the order currency is not MNT (Mongolian Tugrik). This is handled by the _get_compatible_providers() override:

if currency and currency.name != 'MNT': providers = providers.filtered(lambda p: p.code != 'qpay')

Invoice Data Mapping

QPay FieldOdoo Source
invoice_codeProvider field: qpay_invoice_code
sender_invoice_noTransaction reference
amountTransaction amount
callback_urlAuto-generated: {base_url}/payment/qpay/webhook
invoice_descriptionTransaction reference
invoice_receiver_codePartner (customer) email

Token Management

The provider model uses a class-level dictionary cache (_qpay_token_cache) keyed by provider ID. If a request returns a 401 status, the cache is cleared and a new token is acquired automatically.


Webhook Setup

The module automatically registers two HTTP endpoints via the QPayController:

Payment Webhook

POST /payment/qpay/webhook
  • Type: JSON (type='json')
  • Auth: Public (auth='public')
  • CSRF: Disabled (csrf=False)

This endpoint:

  1. Receives the QPay callback data (JSON)
  2. Finds the matching payment.transaction by sender_invoice_no
  3. Calls _handle_notification_data() which triggers _process_notification_data()
  4. The notification handler verifies the payment via POST /v2/payment/check
  5. If payment rows exist, sets the transaction to done; otherwise sets it to pending

Payment Status Check

POST /payment/qpay/check
  • Type: JSON
  • Auth: Public
  • CSRF: Disabled

This endpoint is used by the client-side JavaScript polling. It accepts invoice_id in the JSON body, looks up the transaction, verifies payment with QPay, and returns { paid: true/false }.

The callback URL is automatically generated using self.get_base_url() and does not need manual configuration.


Customization

Extending the Provider

Add custom fields to the QPay provider by inheriting the model:

from odoo import fields, models class PaymentProvider(models.Model): _inherit = 'payment.provider' qpay_custom_field = fields.Char( string='Custom Field', required_if_provider='qpay', )

Add the field to the admin view by inheriting payment_provider_form_qpay:

<record id="custom_qpay_view" model="ir.ui.view"> <field name="model">payment.provider</field> <field name="inherit_id" ref="payment_qpay.payment_provider_form_qpay"/> <field name="arch" type="xml"> <field name="qpay_invoice_code" position="after"> <field name="qpay_custom_field"/> </field> </field> </record>

Extending the Transaction

Override _process_notification_data() to add custom logic after payment verification:

class PaymentTransaction(models.Model): _inherit = 'payment.transaction' def _process_notification_data(self, notification_data): super()._process_notification_data(notification_data) if self.provider_code != 'qpay': return # Custom logic after QPay payment processing

Payment Template

The payment form template (payment_qpay_templates.xml) uses Odoo’s QWeb templating. Customize the template by inheriting it:

<template id="custom_payment_form" inherit_id="payment_qpay.payment_form_qpay"> <xpath expr="//div[@id='qpay-payment-container']" position="inside"> <p>Custom content here</p> </xpath> </template>

API Client Usage

The QPay API methods on the provider model can be called from any Odoo code:

provider = self.env['payment.provider'].search([('code', '=', 'qpay')], limit=1) # Create an invoice invoice = provider._qpay_make_request('/v2/invoice', { 'invoice_code': provider.qpay_invoice_code, 'sender_invoice_no': 'CUSTOM-001', 'amount': 50000, 'callback_url': 'https://yoursite.com/payment/qpay/webhook', }) # Check payment status result = provider._qpay_make_request('/v2/payment/check', { 'object_type': 'INVOICE', 'object_id': invoice['invoice_id'], })

Troubleshooting

QPay not appearing at checkout

  • Verify the module is installed: check Settings > Apps for “QPay Payment Provider”
  • Ensure the provider state is set to Enabled (not Disabled)
  • Make sure the provider is published (check “Published” checkbox)
  • Confirm the order currency is MNT — QPay is automatically hidden for other currencies

”QPay invoice creation failed” error

  • Check your QPay credentials in the provider configuration
  • Verify the Base URL is correct
  • Ensure the Odoo server can make outbound HTTPS requests to merchant.qpay.mn
  • Check Odoo server logs for the full error traceback

Webhook not confirming payments

  • The webhook URL is auto-generated: {odoo_base_url}/payment/qpay/webhook
  • Verify the Odoo base URL is correctly configured in Settings > Technical > System Parameters (web.base.url)
  • Check that the webhook endpoint is publicly accessible
  • Review Odoo server logs for “QPay webhook received” messages

Token authentication errors

  • If you see 401 errors, the module automatically clears the token cache and retries
  • Persistent auth failures indicate incorrect credentials
  • Check that the credentials have not expired or been rotated on the QPay side

Polling stops after 5 minutes

This is by design. The client-side polling has a 5-minute timeout (setTimeout(function() { clearInterval(interval); }, 300000)). If the customer has not paid within 5 minutes, they need to refresh the page to restart polling.


File Structure

payment_qpay/ ├── __manifest__.py # Module manifest (v1.0.0, depends: payment) ├── __init__.py # Module init ├── models/ │ ├── __init__.py │ ├── payment_provider.py # QPay provider (fields, auth, API requests) │ └── payment_transaction.py # Transaction handling (invoice creation, verification) ├── controllers/ │ ├── __init__.py │ └── main.py # HTTP controllers (webhook, payment check) ├── views/ │ ├── payment_provider_views.xml # Admin config form (inherits payment.provider.form) │ └── payment_qpay_templates.xml # Payment form template (QR, bank links, polling) ├── data/ │ └── payment_provider_data.xml # Default QPay provider record ├── .github/ │ └── workflows/ │ └── ci.yml # CI configuration ├── LICENSE └── README.md

Last updated on