Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*__pycache__*
15 changes: 8 additions & 7 deletions payment_btcpayserver/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# BTCPay Server payment gateway for Odoo 18
# BTCPay Server payment gateway for Odoo 19

## This is the module to connect Odoo 18 and BTCPay Server
## This is the module to connect Odoo 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/18.0/payment_btcpayserver/static/description/BTCPay-Odoo-17-featured.png)
![BTCPay Server Banner](https://raw.githubusercontent.com/btcpayserver/odoo/19.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) and for Odoo 17 [here](https://github.com/btcpayserver/odoo/blob/17.0/payment_btcpayserver/README.md).
:::

## 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 `18.0` or downloaded a release tagged with version v18.x
* Make sure you are on branch `19.0` or downloaded a release tagged with version v19.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
Expand Down Expand Up @@ -46,13 +46,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/18.0/payment_btcpayserver/static/description/BTCPayPaymentSettings.png)
![Payment Provider Settings](https://raw.githubusercontent.com/btcpayserver/odoo/19.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/18.0/payment_btcpayserver/static/description/BTCPayLooksLike.png)
![Checkout page example](https://raw.githubusercontent.com/btcpayserver/odoo/19.0/payment_btcpayserver/static/description/BTCPayLooksLike.png)


## Transaction BTCPay Details
Expand All @@ -61,8 +61,9 @@ 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/18.0/payment_btcpayserver/static/description/BtcpayTxDetails.png)
![Transaction details of BTCPay](https://raw.githubusercontent.com/btcpayserver/odoo/19.0/payment_btcpayserver/static/description/BtcpayTxDetails.png)

## Troubleshooting
### The order and transaction status does not get updated to "paid"
If the BTCPay connection generally works, like redirect to BTCPay checkout page (QR-code) then check your odoo logs and make sure PDF generation generally works. If there are errors mentioning wkhtmltopdf then you need to install `wkhtmltopdf` on your server.
>>>>>>> 606e6dc (v19 changes)
8 changes: 4 additions & 4 deletions payment_btcpayserver/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@

{
'name': 'Payment Provider: BTCPay Server',
'summary': 'This module integrates BTCPAY - pay with Bitcoin - with Odoo v18.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': '18.0.1.0',
'version': '19.0.1.0',
'license': 'GPL-3',
'currency': 'USD',
'application': False,
Expand All @@ -38,10 +38,10 @@
'views/payment_transaction_views.xml',
'data/payment_provider_data.xml',
],
'images': ['static/description/BTCPay-Odoo-17-featured.png'],
'images': ['static/description/BTCPay-Odoo-19-featured.png'],
'external_dependencies': {
'python': ['btcpay-python']
},
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
}
}
53 changes: 45 additions & 8 deletions payment_btcpayserver/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class BTCPayController(http.Controller):
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)
tx_sudo = request.env['payment.transaction'].sudo()._search_by_reference('btcpayserver', data)
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')
Expand Down Expand Up @@ -77,26 +77,63 @@ def btcpay_ipn(self, **post):
""" BTCPay IPN. """
_logger.info('BTCPAY IPN RECEIVED... ')
data = json.loads(request.httprequest.data)
_logger.info("%s", pprint.pformat(data))
_logger.info("IPN Data: %s", pprint.pformat(data))
try:
notification_data = {"reference": data['data']['orderId'],
"invoiceID": data['data']['id']}
# Extract order ID and invoice ID from the data
# The structure might vary depending on the BTCPay Server version
# Try different possible structures
order_id = None
invoice_id = None

# Try to extract from data['data'] structure (v18 structure)
if 'data' in data and isinstance(data['data'], dict):
if 'orderId' in data['data']:
order_id = data['data']['orderId']
if 'id' in data['data']:
invoice_id = data['data']['id']

# If not found, try to extract directly from data (possible v19 structure)
if not order_id and 'orderId' in data:
order_id = data['orderId']
if not invoice_id and 'id' in data:
invoice_id = data['id']

# If still not found, try to extract from other possible structures
if not order_id and 'order_id' in data:
order_id = data['order_id']
if not invoice_id and 'invoice_id' in data:
invoice_id = data['invoice_id']

# Log the extracted values
_logger.info("Extracted order_id: %s, invoice_id: %s", order_id, invoice_id)

if not order_id or not invoice_id:
_logger.error("Could not extract order_id or invoice_id from the IPN data")
return ''

notification_data = {"reference": order_id,
"invoiceID": invoice_id}
# Check the origin and integrity of the notification
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data('btcpayserver', notification_data)
tx_sudo = request.env['payment.transaction'].sudo()._search_by_reference('btcpayserver', notification_data)
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))

# Get the amount from the transaction and ensure it's in the correct format
amount = tx_sudo.amount
_logger.info("Transaction amount: %s", amount)

notification_data = {"reference": fetched_invoice['orderId'],
"status": fetched_invoice['status'],
"invoiceID": fetched_invoice['id'],
"txid": fetched_invoice['url']}
"txid": fetched_invoice['url'],
"amount": amount}

# Handle the notification data
tx_sudo._handle_notification_data('btcpayserver', notification_data)
tx_sudo._process('btcpayserver', notification_data)
except ValidationError: # Acknowledge the notification to avoid getting spammed
_logger.exception("Unable to handle the notification data; skipping to acknowledge")
return ''
Expand All @@ -109,4 +146,4 @@ def btcpay_return_from_redirect(self, **data):
IPN anyway, so just show the user the order confirmation / payment status page.
"""
_logger.info("BTCPay: user returned to shop after payment")
return request.redirect('/payment/status')
return request.redirect('/payment/status')
71 changes: 50 additions & 21 deletions payment_btcpayserver/models/payment_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,71 +57,75 @@ def _get_specific_rendering_values(self, processing_values):
'notify_url': base_url + self.notify_url,
}

def _get_tx_from_notification_data(self, provider_code, notification_data):
def _search_by_reference(self, provider_code, payment_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
:param dict payment_data: The payment 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))
tx = super()._search_by_reference(provider_code, payment_data)
_logger.info('SEARCH BY REFERENCE Payment_data %s', pprint.pformat(payment_data))
if provider_code != 'btcpayserver' or len(tx) == 1:
return tx

reference = notification_data.get('reference')
reference = payment_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.
def _process(self, provider_code, payment_data):
""" Override of payment to process the transaction based on 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.
:param dict payment_data: The payment data sent by the provider.
:return: The transaction.
:rtype: recordset of `payment.transaction`
"""
tx = self._get_tx_from_notification_data(provider_code, notification_data)
tx._process_notification_data(notification_data)
tx = super()._process(provider_code, payment_data)
if provider_code != 'btcpayserver':
return tx

tx = self._search_by_reference(provider_code, payment_data)
tx._apply_updates(payment_data)
return tx

def _process_notification_data(self, notification_data):
def _apply_updates(self, payment_data):
""" Override of payment to process the transaction based on BTCPay data.

Note: self.ensure_one()

: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)
super()._apply_updates(payment_data)
if self.provider_code != 'btcpayserver':
return

_logger.info("_process_notification_data %s", pprint.pformat(notification_data))
txn_id = notification_data.get('reference')
if not all(txn_id):
_logger.info("_apply_updates %s", pprint.pformat(payment_data))
txn_id = payment_data.get('reference')
if not txn_id:
raise ValidationError(
"BTCPay: " + _("Missing value for txn_id (%(txn_id)s)).", txn_id=txn_id))

self.provider_reference = txn_id
self.btcpay_txid = notification_data.get('txid')
self.btcpay_status = notification_data.get('status')
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'))
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')
self.btcpay_invoiceId = payment_data.get('invoiceID')
elif self.btcpay_status in ['cancel','cancelled']:
self._set_canceled()
elif self.btcpay_status in ['invalid']:
Expand All @@ -131,4 +135,29 @@ def _process_notification_data(self, notification_data):
)
self._set_error(
"BTCPay: " + _("Received data with invalid payment status: %s", self.btcpay_status)
)
)

def _extract_amount_data(self, payment_data):
"""Extract the amount, currency and rounding precision from the payment data.

This method must be overridden by providers to parse the amount data from the payment data.
If the provider returns `None`, the amount validation is skipped.

:param dict payment_data: The payment data sent by the provider.
:return: The amount data, in the {amount: float, currency_code: str, precision_digits: int}
format.
:rtype: dict|None
"""
_logger.info("_extract_amount_data %s", pprint.pformat(payment_data))
if self.provider_code != 'btcpayserver' or not payment_data.get('amount'):
return super()._extract_amount_data(payment_data)

# Extract amount from payment data
amount = payment_data.get('amount')

# Return the amount data in the required format
return {
'amount': float(amount),
'currency_code': self.currency_id.name,
'precision_digits': self.currency_id.decimal_places,
}
4 changes: 2 additions & 2 deletions payment_btcpayserver/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ <h2 class="oe_slogan">Requirements</h2>
<div class="oe_span12">
<p class="oe_mt32">
<ul>
<li>Odoo 17 running</li>
<li>Odoo 19 running</li>
<li>eCommerce module enabled</li>
<li>You have a BTCPay Server version 1.10.0 or later, either <a href="https://docs.btcpayserver.org/Deployment/">self-hosted</a> or <a href="https://docs.btcpayserver.org/Deployment/ThirdPartyHosting/">hosted by a third-party</a></li>
<li><a href="https://docs.btcpayserver.org/RegisterAccount/" class="">You've a registered account on the instance</a></li>
Expand All @@ -84,7 +84,7 @@ <h2 class="oe_slogan">Install the module</h2>
<p class="oe_mt32">
<ul>
<li>Clone our [repository](https://github.com/btcpayserver/odoo) or download the .zip from the [releases page](https://github.com/btcpayserver/odoo/releases)
<li>Make sure you are on branch `17.0` or downloaded a release tagged with version v17.x
<li>Make sure you are on branch `19.0` or downloaded a release tagged with version v19.x
<li>Place the `payment_btcpayserver` directory in your Odoo addons directory
<li>Install dependencies by running `pip install -r requirements.txt` (from inside the `payment_btcpayserver` directory)
<li>Restart Odoo
Expand Down