2 Commits
Author SHA1 Message Date
ndeetandGitHub 225d4c33c5 Odoo 19 compatibilty (#16)
* Odoo 19 compatibilty.

* Update docs.
2026-03-30 16:36:20 +02:00
ndeet b935000fda Odoo 18.0 compatibility. 2025-01-27 14:50:53 +01:00
5 changed files with 114 additions and 115 deletions
+16 -11
View File
@@ -1,16 +1,20 @@
# BTCPay Server payment gateway for Odoo 17
# BTCPay Server payment gateway for Odoo 18 / 19
## This is the module to connect Odoo 17 and BTCPay Server
## This is the module to connect Odoo 18 / 19 and BTCPay Server
This module allows you to accept bitcoin (and other cryptocurrency) payments in your Odoo e-commerce store.
![BTCPay Server Banner](https://raw.githubusercontent.com/btcpayserver/odoo/17.0/payment_btcpayserver/static/description/BTCPay-Odoo-17-featured.png)
![BTCPay Server Banner](https://raw.githubusercontent.com/btcpayserver/odoo/18.0/payment_btcpayserver/static/description/BTCPay-Odoo-17-featured.png)
:::tip
If you use Odoo 16 you can find the documentation [here](https://github.com/btcpayserver/odoo/blob/16.0/payment_btcpayserver/README.md).
If you use Odoo 16 you can find the documentation [here](https://github.com/btcpayserver/odoo/blob/16.0/payment_btcpayserver/README.md) and for Odoo 17 [here](https://github.com/btcpayserver/odoo/blob/17.0/payment_btcpayserver/README.md).
:::
:::tip
The following instructions work for Odoo 18 and 19. You just need to make sure to download the matching branch release or use the matching branch (18.0 or 19.0) mentioned in the steps below.
:::
## Install the module
* Clone our [repository](https://github.com/btcpayserver/odoo) or download the .zip from the [releases page](https://github.com/btcpayserver/odoo/releases)
* Make sure you are on branch `17.0` or downloaded a release tagged with version v17.x
* Make sure you are on branch `19.0` (or `18.0`) or downloaded a [release](https://github.com/btcpayserver/odoo/releases) tagged with version v19.x (or v18.x)
* Place the `payment_btcpayserver` directory in your Odoo addons directory
* Install dependencies by running `pip install -r requirements.txt` (from inside the `payment_btcpayserver` directory)
* Restart Odoo
@@ -25,15 +29,16 @@ If you use Odoo 16 you can find the documentation [here](https://github.com/btcp
In the BTCPay settings form, tab "Credentials":
* Set field "State" to enabled
* Set field "BTCPay Server URL" as test or live URL including https://. Example URL: https://testnet.demo.btcpayserver.org
* Get a pairing code from your BTCPay Server store: Settings -> Access Tokens
* Get a pairing code from your BTCPay Server store:
* Go to: Settings -> Access Tokens
* Click on "Create Token" button
* Label: enter e.g. "My odoo store"
* Public Key: leave empty
* Click on "Request Pairing" button, on next page click "Approve" button
* At the top copy the code next to "Server initiated pairing code", e.g. "hg7z8wN"
* Back in Odoo, paste the code into "Pairing Code" field
* Back in Odoo: paste the code into "Pairing Code" field
* Hit Tab key on your keyboard (or click on another field) and the pairing process will start automatically
* When the pairing is successful the "Token" and "Private Key" field will be filled automatically
* When the pairing is successful the "Token" and "Private Key" field will be filled automatically (see screenshot below)
* Field Facade, keep default 'merchant'.
On the tab "Configuration":
@@ -46,13 +51,13 @@ Check the payment method is enabled:
Congrats, all done. Do some testing to be sure all works.
![Payment Provider Settings](https://raw.githubusercontent.com/btcpayserver/odoo/17.0/payment_btcpayserver/static/description/BTCPayPaymentSettings.png)
![Payment Provider Settings](https://raw.githubusercontent.com/btcpayserver/odoo/18.0/payment_btcpayserver/static/description/BTCPayPaymentSettings.png)
## How does the payment page look?
During the checkout the customers will have the option to select the payment method "Pay with Bitcoin / Lightning Network". After selecting they will be redirected to the BTCPay checkout page as shown below.
![Checkout page example](https://raw.githubusercontent.com/btcpayserver/odoo/17.0/payment_btcpayserver/static/description/BTCPayLooksLike.png)
![Checkout page example](https://raw.githubusercontent.com/btcpayserver/odoo/18.0/payment_btcpayserver/static/description/BTCPayLooksLike.png)
## Transaction BTCPay Details
@@ -61,7 +66,7 @@ In transaction object, you will find more technical information about this metho
* Invoice Id: the id of the invoice for which you want to fetch an event token
* Transaction Status: That indicates state of transaction
![Transaction details of BTCPay](https://raw.githubusercontent.com/btcpayserver/odoo/17.0/payment_btcpayserver/static/description/BtcpayTxDetails.png)
![Transaction details of BTCPay](https://raw.githubusercontent.com/btcpayserver/odoo/18.0/payment_btcpayserver/static/description/BtcpayTxDetails.png)
## Troubleshooting
### The order and transaction status does not get updated to "paid"
+3 -3
View File
@@ -21,11 +21,11 @@
{
'name': 'Payment Provider: BTCPay Server',
'summary': 'This module integrates BTCPAY - pay with Bitcoin - with Odoo v17.0',
'summary': 'This module integrates BTCPAY - pay with Bitcoin - with Odoo v19.0',
'author': 'BTCPay Server team and contributors',
'website': 'https://github.com/btcpayserver/odoo',
'category': 'Accounting/Payment Providers',
'version': '17.0.2.0',
'version': '19.0.1.0',
'license': 'GPL-3',
'currency': 'USD',
'application': False,
@@ -44,4 +44,4 @@
},
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
}
}
+46 -32
View File
@@ -19,24 +19,21 @@
#
# ******************************************************************************
import logging
import json
import pprint
import requests
import werkzeug
from werkzeug import urls
from werkzeug.exceptions import Forbidden
from odoo import _, http, SUPERUSER_ID
from odoo import _, http
from odoo.exceptions import ValidationError
from odoo.http import request
from odoo.tools import html_escape
import json
from odoo.addons.payment.logging import get_payment_logger
from ..models.libs.client import BTCPayClient
_logger = logging.getLogger(__name__)
_logger = get_payment_logger(__name__)
class BTCPayController(http.Controller):
@@ -44,11 +41,22 @@ class BTCPayController(http.Controller):
_notify_url = '/payment/btcpay/ipn'
_return_url = '/payment/btcpay/return'
@http.route(_checkout_url, type='http', auth='public', csrf=False, website=True)
@http.route(_checkout_url, type='http', auth='public', csrf=False, website=True)
def checkout(self, **data):
_logger.info("CHECKOUT: notification received from BTCPay with data:\n%s", pprint.pformat(data))
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data('btcpayserver', data)
_logger.info("CHECKOUT: received data:\n%s", pprint.pformat(data))
# Look up the transaction by reference
reference = data.get('reference')
tx_sudo = request.env['payment.transaction'].sudo().search([
('reference', '=', reference),
('provider_code', '=', 'btcpayserver'),
], limit=1)
if not tx_sudo:
raise ValidationError(
_("BTCPay: No transaction found matching reference %s.", reference)
)
provider = tx_sudo.provider_id
notification_url = str(data.get('notify_url')).replace("http://", "https://")
base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
@@ -72,41 +80,47 @@ class BTCPayController(http.Controller):
_logger.info('Invoice %s \n NOTIFY URL: %s', invoice, notification_url)
return werkzeug.utils.redirect(invoice['url'])
@http.route(_notify_url, type='json', auth='public', csrf=False)
@http.route(_notify_url, type='jsonrpc', auth='public', csrf=False)
def btcpay_ipn(self, **post):
""" BTCPay IPN. """
_logger.info('BTCPAY IPN RECEIVED... ')
_logger.info('BTCPAY IPN RECEIVED...')
data = json.loads(request.httprequest.data)
_logger.info("%s", pprint.pformat(data))
try:
notification_data = {"reference": data['data']['orderId'],
"invoiceID": data['data']['id']}
# Check the origin and integrity of the notification
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data('btcpayserver', notification_data)
reference = data['data']['orderId']
invoice_id = data['data']['id']
# Look up the transaction by reference
tx_sudo = request.env['payment.transaction'].sudo().search([
('reference', '=', reference),
('provider_code', '=', 'btcpayserver'),
], limit=1)
if not tx_sudo:
_logger.warning("No transaction found matching reference %s.", reference)
return ''
provider = tx_sudo.provider_id
client = BTCPayClient(host=provider.btcpay_location, pem=provider.btcpay_privateKey,
tokens={provider.btcpay_facade: provider.btcpay_token})
fetched_invoice = client.get_invoice(notification_data['invoiceID'])
_logger.info('fetched_invoice = %s',pprint.pformat(fetched_invoice))
fetched_invoice = client.get_invoice(invoice_id)
_logger.info('fetched_invoice = %s', pprint.pformat(fetched_invoice))
notification_data = {"reference": fetched_invoice['orderId'],
"status": fetched_invoice['status'],
"invoiceID": fetched_invoice['id'],
"txid": fetched_invoice['url']}
payment_data = {
"reference": fetched_invoice['orderId'],
"status": fetched_invoice['status'],
"invoiceID": fetched_invoice['id'],
"txid": fetched_invoice['url'],
}
# Handle the notification data
tx_sudo._handle_notification_data('btcpayserver', notification_data)
except ValidationError: # Acknowledge the notification to avoid getting spammed
# Use the new Odoo 19 _process() pipeline
tx_sudo._process('btcpayserver', payment_data)
except ValidationError:
_logger.exception("Unable to handle the notification data; skipping to acknowledge")
return ''
@http.route(_return_url, type='http', auth="public", methods=['GET'], crsf=False, save_session=False)
@http.route(_return_url, type='http', auth="public", methods=['GET'], csrf=False, save_session=False)
def btcpay_return_from_redirect(self, **data):
""" BTCPay return
We could process and check the invoice status here but there is no need to as the status get's updated via
IPN anyway, so just show the user the order confirmation / payment status page.
"""
""" BTCPay return """
_logger.info("BTCPay: user returned to shop after payment")
return request.redirect('/payment/status')
return request.redirect('/payment/status')
@@ -1,10 +1,11 @@
import logging
from odoo import _, api, fields, models
from .libs.client import BTCPayClient
from .libs import crypto
_logger = logging.getLogger(__name__)
from odoo.addons.payment.logging import get_payment_logger
_logger = get_payment_logger(__name__)
class PaymentProvider(models.Model):
@@ -22,14 +23,13 @@ class PaymentProvider(models.Model):
def create(self, values_list):
if self.code == 'btcpayserver':
values_list['btcpay_privateKey'] = crypto.generate_privakey()
values_list['btcpay_privateKey'] = crypto.generate_privkey()
return super(PaymentProvider, self).create(values_list)
@api.onchange('btcpay_pairingCode')
def _onchange_pairingCode(self):
if not self.btcpay_token and self.code == 'btcpayserver' and not self.btcpay_pairingCode == '':
#_logger.info("ONCHANGE PAIRING CODE***SELF: %s %s %s", self.btcpay_location, self.btcpay_privateKey, self.btcpay_pairingCode)
self.btcpay_privateKey = crypto.generate_privkey()
client = BTCPayClient(host=self.btcpay_location, pem=self.btcpay_privateKey)
token = client.pair_client(self.btcpay_pairingCode)
@@ -39,13 +39,10 @@ class PaymentProvider(models.Model):
def _onchange_token(self):
if self.code == 'btcpayserver':
self.btcpay_pairingCode = ''
#_logger.info("ONCHANGE TOKEN")
@api.onchange('btcpay_location')
def _onchange_location(self):
if self.code == 'btcpayserver':
self.btcpay_token = ''
#_logger.info("ONCHANGE LOCATION ***SELF: %s %s %s", self.btcpay_location, self.btcpay_privateKey, self.btcpay_pairingCode)
self.btcpay_privateKey = ''
self.btcpay_pairingCode = ''
self.btcpay_pairingCode = ''
@@ -1,13 +1,12 @@
import logging
import pprint
from werkzeug import urls
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment.logging import get_payment_logger
_logger = logging.getLogger(__name__)
_logger = get_payment_logger(__name__)
class PaymentTransaction(models.Model):
@@ -21,7 +20,7 @@ class PaymentTransaction(models.Model):
notify_url = 'payment/btcpay/ipn'
def _get_specific_rendering_values(self, processing_values):
""" Override of payment to return Specific rendering values.
""" Override of payment to return BTCPay-specific rendering values.
Note: self.ensure_one() from `_get_processing_values`
@@ -29,14 +28,12 @@ class PaymentTransaction(models.Model):
:return: The dict of provider-specific processing values
:rtype: dict
"""
res = super()._get_specific_rendering_values(processing_values)
if self.provider_code != 'btcpayserver':
return res
base_url = self.provider_id.get_base_url()
_logger.info('Hola! API URL: %s', processing_values)
partner_first_name, partner_last_name = payment_utils.split_partner_name(self.partner_name)
return {
@@ -57,79 +54,65 @@ class PaymentTransaction(models.Model):
'notify_url': base_url + self.notify_url,
}
def _get_tx_from_notification_data(self, provider_code, notification_data):
""" Override of payment to find the transaction based on BTCPay data.
:param str provider_code: The code of the provider that handled the transaction
:param dict notification_data: The notification data sent by the provider
:return: The transaction if found
:rtype: recordset of `payment.transaction`
:raise: ValidationError if the data match no transaction
"""
tx = super()._get_tx_from_notification_data(provider_code, notification_data)
_logger.info('GET TX FROM NOTIFICATION Notification_data %s', pprint.pformat(notification_data))
if provider_code != 'btcpayserver' or len(tx) == 1:
return tx
reference = notification_data.get('reference')
tx = self.search([('reference', '=', reference), ('provider_code', '=', 'btcpayserver')])
if not tx:
raise ValidationError(
"BTCPay: " + _("No transaction found matching reference %s.", reference)
)
return tx
def _handle_notification_data(self, provider_code, notification_data):
""" Match the transaction with the notification data, update its state and return it.
@api.model
def _extract_reference(self, provider_code, payment_data):
""" Override of payment to extract the transaction reference from BTCPay data.
:param str provider_code: The code of the provider handling the transaction.
:param dict notification_data: The notification data sent by the provider.
:return: The transaction.
:rtype: recordset of `payment.transaction`
:param dict payment_data: The payment data sent by the provider.
:return: The transaction reference.
:rtype: str
"""
tx = self._get_tx_from_notification_data(provider_code, notification_data)
tx._process_notification_data(notification_data)
tx._execute_callback()
return tx
if provider_code != 'btcpayserver':
return super()._extract_reference(provider_code, payment_data)
def _process_notification_data(self, notification_data):
return payment_data.get('reference')
def _extract_amount_data(self, payment_data):
""" Override of payment to skip amount validation for BTCPay.
BTCPay invoices handle amount validation on the BTCPay server side,
so we skip the Odoo-side validation.
:param dict payment_data: The payment data sent by the provider.
:return: None to skip validation.
:rtype: None
"""
if self.provider_code != 'btcpayserver':
return super()._extract_amount_data(payment_data)
return None
def _apply_updates(self, payment_data):
""" Override of payment to process the transaction based on BTCPay data.
Note: self.ensure_one()
Note: self.ensure_one() from `_process`
:param dict notification_data: The notification data sent by the provider
:param dict payment_data: The payment data sent by the provider.
:return: None
:raise: ValidationError if inconsistent data were received
"""
super()._process_notification_data(notification_data)
if self.provider_code != 'btcpayserver':
return
return super()._apply_updates(payment_data)
_logger.info("_process_notification_data %s", pprint.pformat(notification_data))
txn_id = notification_data.get('reference')
if not all(txn_id):
raise ValidationError(
"BTCPay: " + _("Missing value for txn_id (%(txn_id)s)).", txn_id=txn_id))
_logger.info("BTCPay _apply_updates: %s", pprint.pformat(payment_data))
self.provider_reference = txn_id
self.btcpay_txid = notification_data.get('txid')
self.btcpay_status = notification_data.get('status')
self.provider_reference = payment_data.get('reference')
self.btcpay_txid = payment_data.get('txid')
self.btcpay_status = payment_data.get('status')
if self.btcpay_status in ['paid','processing']:
self._set_pending(state_message=notification_data.get('pending_reason'))
if self.btcpay_status in ['paid', 'processing']:
self._set_pending(state_message=payment_data.get('pending_reason'))
elif self.btcpay_status in ['confirmed', 'complete']:
self._set_done()
confirmed_orders = self._check_amount_and_confirm_order()
confirmed_orders._send_order_confirmation_mail()
elif self.btcpay_status in ['new']:
self.btcpay_invoiceId = notification_data.get('invoiceID')
elif self.btcpay_status in ['cancel','cancelled']:
self.btcpay_invoiceId = payment_data.get('invoiceID')
elif self.btcpay_status in ['cancel', 'cancelled']:
self._set_canceled()
elif self.btcpay_status in ['invalid']:
_logger.info(
"received data with invalid payment status (%s) for transaction with reference %s",
"Received data with invalid payment status (%s) for transaction with reference %s",
self.btcpay_status, self.reference
)
self._set_error(
"BTCPay: " + _("Received data with invalid payment status: %s", self.btcpay_status)
)
)