10 Commits
Author SHA1 Message Date
ndeet 95c6cd53d9 Prepare for marketplace submission, rename module; new release. (#13) 2024-08-21 17:34:18 +02:00
ndeet 1421168374 Bumping version. 2024-07-31 14:22:32 +02:00
ndeet 600a054314 Make sure email is passed, also if used in PoS context.
(cherry picked from commit 6042284704)
2024-07-31 14:20:25 +02:00
ndeet bc934a0745 Updating teaser image. 2024-07-17 16:53:38 +02:00
ndeet c5cd74e03f Minor clarification. 2024-07-09 11:13:11 +02:00
ndeet 9e2475f415 Update docs to latest changes. 2024-07-09 10:26:02 +02:00
ndeet 488aea8771 Remove customizable return url and redirect the user to order confirmation / payment status page as it would be expected.
(cherry picked from commit 6a22f82a73)
2024-07-08 16:40:27 +02:00
ndeet 0f31982616 Fix IPN processing to cover additional invoice states.
(cherry picked from commit 17cb4f7814)
2024-07-08 11:13:03 +02:00
ndeet c32776ba60 Change version to match 17.0 branch scheme. 2024-07-08 09:22:04 +02:00
ndeet 43636e52e3 Fix typo. 2024-07-08 09:19:18 +02:00
14 changed files with 174 additions and 397 deletions

No files matched your search

-12
View File
@@ -1,12 +0,0 @@
# BTCPay Server payment gateway for Odoo 16, 17 and 18
## Accept bitcoin payments on Odoo 16, 17 and 18
This odoo addon allows you to accept bitcoin and Lightning Network (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)
## How to install
Please find the installation instructions here:
- [Odoo 18 documentation](https://github.com/btcpayserver/odoo/blob/18.0/payment_btcpayserver/README.md)
- [Odoo 17 documentation](https://github.com/btcpayserver/odoo/blob/17.0/payment_btcpayserver/README.md)
- [Odoo 16 documentation](https://github.com/btcpayserver/odoo/blob/16.0/payment_btcpayserver/README.md)
+8 -22
View File
@@ -1,16 +1,12 @@
# BTCPay Server payment gateway for Odoo 18
# BTCPay Server payment gateway for Odoo 16
## This is the module to connect Odoo 18 and BTCPay Server
## This is the module to connect Odoo 16 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)
:::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).
:::
![BTCPay Server Banner](../payment_btcpayserver/static/description/BTCPay-Odoo-16-featured.png)
## 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 `16.0` or downloaded a release tagged with version v16.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
@@ -37,22 +33,16 @@ In the BTCPay settings form, tab "Credentials":
* Field Facade, keep default 'merchant'.
On the tab "Configuration":
* Make sure field "Payment Journal" is set to "Bank", otherwise you can click the dropdown and click on the suggestion "Bank"
* Set field "Payment Journal" to "Bank", you can click the dropdown and click on the suggestion "Bank"
* Now you can **save** the settings
Check the payment method is enabled:
* Go to **Website** -> **Configuration** -> **Payment Methods**
* Make sure "Pay with Bitcoin / Lightning Network" is active
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](../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)
![Payment Provider](../payment_btcpayserver/static/description/BTCPayLooksLike.png)
## Transaction BTCPay Details
@@ -61,8 +51,4 @@ 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)
## 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.
![Transaction Btcpay Details](../payment_btcpayserver/static/description/BtcpayTxDetails.png)
+4 -4
View File
@@ -6,9 +6,9 @@ from . import models
from odoo.addons.payment import setup_provider, reset_payment_provider
def post_init_hook(env):
setup_provider(env, 'btcpayserver')
def post_init_hook(cr, registry):
setup_provider(cr, registry, 'btcpayserver')
def uninstall_hook(env):
reset_payment_provider(env, 'btcpayserver')
def uninstall_hook(cr, registry):
reset_payment_provider(cr, registry, 'btcpayserver')
+5 -7
View File
@@ -1,7 +1,7 @@
#******************************************************************************
# PAYMENT BTCPAY SERVER FOR ODOO
#
# Copyright (C) 2024 Susanna Fort <susannafm@gmail.com>, ndeet
# Copyright (C) 2023 Susanna Fort <susannafm@gmail.com>, ndeet
#
#******************************************************************************
#
@@ -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 v16.0',
'author': 'BTCPay Server team and contributors',
'website': 'https://github.com/btcpayserver/odoo',
'category': 'Accounting/Payment Providers',
'version': '18.0.1.1',
'version': '16.0.2.0',
'license': 'GPL-3',
'currency': 'USD',
'application': False,
@@ -38,11 +38,9 @@
'views/payment_transaction_views.xml',
'data/payment_provider_data.xml',
],
'images': ['static/description/BTCPay-Odoo-17-featured.png'],
# The BTCPay client (legacy BitPay API) is vendored in models/libs and
# only requires 'ecdsa' (plus 'requests', shipped with Odoo).
'images': ['static/description/BTCPay-Odoo-16-featured.png'],
'external_dependencies': {
'python': ['ecdsa']
'python': ['btcpay-python']
},
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
+53 -244
View File
@@ -19,17 +19,23 @@
#
# ******************************************************************************
import json
import logging
import pprint
import requests
import werkzeug
from werkzeug import urls
from werkzeug.exceptions import Forbidden
from odoo import _, http
from odoo import _, http, SUPERUSER_ID
from odoo.exceptions import ValidationError
from odoo.http import request
from odoo.tools import html_escape
import json
from ..models.libs.client import BTCPayClient
_logger = logging.getLogger(__name__)
@@ -38,261 +44,64 @@ class BTCPayController(http.Controller):
_notify_url = '/payment/btcpay/ipn'
_return_url = '/payment/btcpay/return'
@staticmethod
def _btcpay_client(provider):
return BTCPayClient(
host=provider.btcpay_location,
pem=provider.btcpay_privateKey,
tokens={provider.btcpay_facade: provider.btcpay_token},
)
@staticmethod
def _lock_transaction(tx_sudo):
"""Lock a transaction until the current database transaction ends.
Checkout and IPN requests can arrive concurrently. Serializing them on
the payment transaction prevents two checkout requests from creating
separate invoices and makes an IPN re-check the invoice ID after a
checkout replaces an expired invoice.
"""
tx_sudo.ensure_one()
tx_sudo.flush_recordset()
tx_sudo.env.cr.execute(
"SELECT id FROM payment_transaction WHERE id = %s FOR UPDATE",
[tx_sudo.id],
)
tx_sudo.invalidate_recordset([
'state', 'btcpay_invoiceId', 'provider_reference',
])
@staticmethod
def _ensure_invoice_id_is_unique(tx_sudo, invoice_id):
"""Reject an invoice ID already linked to another transaction."""
other_tx = tx_sudo.env['payment.transaction'].sudo().search([
('id', '!=', tx_sudo.id),
('provider_code', '=', 'btcpayserver'),
'|',
('btcpay_invoiceId', '=', invoice_id),
('provider_reference', '=', invoice_id),
], limit=1)
if other_tx:
raise ValidationError(_(
"BTCPay: The invoice is already linked to another transaction."
))
@classmethod
def _validate_invoice_binding(
cls, tx_sudo, incoming_invoice_id, incoming_order_id, fetched_invoice):
"""Bind the untrusted IPN payload to one authenticated invoice and transaction."""
if not isinstance(fetched_invoice, dict):
raise ValidationError(_("BTCPay: Received an invalid invoice response."))
fetched_invoice_id = fetched_invoice.get('id')
fetched_order_id = fetched_invoice.get('orderId')
if not (
incoming_invoice_id
and incoming_invoice_id == fetched_invoice_id
and incoming_invoice_id == tx_sudo.btcpay_invoiceId
and incoming_invoice_id == tx_sudo.provider_reference
):
raise ValidationError(_(
"BTCPay: The notification invoice does not match the transaction."
))
if not (
incoming_order_id
and incoming_order_id == fetched_order_id
and incoming_order_id == tx_sudo.reference
):
raise ValidationError(_(
"BTCPay: The notification order does not match the transaction."
))
cls._ensure_invoice_id_is_unique(tx_sudo, incoming_invoice_id)
@classmethod
def _find_transaction_by_invoice_id(cls, invoice_id):
"""Find exactly one BTCPay transaction from its stored invoice ID."""
if not isinstance(invoice_id, str) or not invoice_id:
raise ValidationError(_("BTCPay: Missing invoice ID in notification data."))
txs_sudo = request.env['payment.transaction'].sudo().search([
('provider_code', '=', 'btcpayserver'),
('btcpay_invoiceId', '=', invoice_id),
], limit=2)
if len(txs_sudo) != 1:
raise ValidationError(_(
"BTCPay: No unique transaction found matching the invoice ID."
))
return txs_sudo
@staticmethod
def _validate_invoice_amount(tx_sudo, invoice):
"""Reject reuse of an invoice with a different amount or currency."""
try:
amount = float(invoice.get('price'))
except (AttributeError, TypeError, ValueError):
amount = None
if (
amount is None
or invoice.get('currency') != tx_sudo.currency_id.name
or tx_sudo.currency_id.compare_amounts(amount, tx_sudo.amount) != 0
):
raise ValidationError(_(
"BTCPay: The existing invoice amount or currency does not match the transaction."
))
@classmethod
def _validate_existing_invoice(cls, tx_sudo, invoice_id, invoice):
"""Validate a stored invoice before redirecting the customer to it."""
cls._validate_invoice_binding(
tx_sudo, invoice_id, tx_sudo.reference, invoice,
)
if not isinstance(invoice.get('status'), str) or not invoice['status']:
raise ValidationError(_("BTCPay: The existing invoice response is incomplete."))
@classmethod
def _validate_created_invoice(cls, tx_sudo, invoice):
"""Validate a newly created invoice before storing its identity."""
if not isinstance(invoice, dict):
raise ValidationError(_("BTCPay: Received an invalid invoice response."))
invoice_id = invoice.get('id')
if (
not isinstance(invoice_id, str)
or not invoice_id
or not isinstance(invoice.get('url'), str)
or not invoice['url']
or invoice.get('orderId') not in (None, tx_sudo.reference)
):
raise ValidationError(_("BTCPay: The created invoice response is incomplete."))
cls._ensure_invoice_id_is_unique(tx_sudo, invoice_id)
@http.route(_checkout_url, type='http', auth='public', methods=['POST'],
csrf=False, website=True)
@http.route(_checkout_url, type='http', auth='public', csrf=False, website=True)
def checkout(self, **data):
""" Create the BTCPay invoice and redirect the buyer to it.
The redirect form only carries the transaction reference. The invoice
price, currency and buyer details are read from the transaction
server-side and are never taken from the (client-controlled) request
data, so a buyer cannot have an invoice created for a tampered amount.
"""
_logger.info("BTCPay: checkout request for transaction %s", data.get('reference'))
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data(
'btcpayserver', data)
self._lock_transaction(tx_sudo)
if tx_sudo.state != 'draft':
# Already processed (e.g. double submission): fall back to the
# generic payment status page.
return request.redirect('/payment/status')
_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)
provider = tx_sudo.provider_id
base_url = provider.get_base_url()
client = self._btcpay_client(provider)
notification_url = str(data.get('notify_url')).replace("http://", "https://")
base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
redirect_url = urls.url_join(base_url, self._return_url)
client = BTCPayClient(host=provider.btcpay_location, pem=provider.btcpay_privateKey, tokens={provider.btcpay_facade: provider.btcpay_token})
invoice = client.create_invoice(
{"price": data.get('amount'),
"currency": data.get('currency_id'),
"orderId": data.get('reference'),
"token": provider.btcpay_token,
"redirectURL": redirect_url,
"notificationURL": notification_url,
"extendedNotifications": True,
"buyer": {"email": data.get('email') or 'noemailavailable@example.com',
"name": data.get('name'),
"address1": data.get('street'),
"locality": data.get('city'),
"postalCode": data.get('zip'),
"country": data.get('country'),
"notify": False}})
_logger.info('Invoice %s \n NOTIFY URL: %s', invoice, notification_url)
return werkzeug.utils.redirect(invoice['url'])
# A browser retry must not create a second payable invoice. Reuse the
# stored invoice while it remains payable. If it expired, the new ID is
# stored before the lock is released, so late notifications for the old
# invoice can no longer select this transaction.
if tx_sudo.btcpay_invoiceId:
existing_invoice = client.get_invoice(tx_sudo.btcpay_invoiceId)
self._validate_existing_invoice(
tx_sudo, tx_sudo.btcpay_invoiceId, existing_invoice,
)
if existing_invoice['status'].lower() != 'expired':
self._validate_invoice_amount(tx_sudo, existing_invoice)
if (
not isinstance(existing_invoice.get('url'), str)
or not existing_invoice['url']
):
raise ValidationError(_(
"BTCPay: The existing invoice response is incomplete."
))
_logger.info(
"BTCPay: reusing invoice %s for transaction %s",
tx_sudo.btcpay_invoiceId, tx_sudo.reference,
)
return request.redirect(existing_invoice['url'], local=False)
invoice = client.create_invoice({
"price": tx_sudo.amount,
"currency": tx_sudo.currency_id.name,
"orderId": tx_sudo.reference,
"token": provider.btcpay_token,
"redirectURL": urls.url_join(base_url, self._return_url),
"notificationURL": urls.url_join(base_url, self._notify_url),
"extendedNotifications": True,
"buyer": {
"email": tx_sudo.partner_email or 'noemailavailable@example.com',
"name": tx_sudo.partner_name,
"address1": tx_sudo.partner_address,
"locality": tx_sudo.partner_city,
"postalCode": tx_sudo.partner_zip,
"country": tx_sudo.partner_country_id.code,
"notify": False,
},
})
self._validate_created_invoice(tx_sudo, invoice)
tx_sudo.write({
'btcpay_invoiceId': invoice['id'],
'provider_reference': invoice['id'],
})
_logger.info("BTCPay: created invoice %s for transaction %s",
invoice['id'], tx_sudo.reference)
return request.redirect(invoice['url'], local=False)
@http.route(_notify_url, type='json', auth='public', methods=['POST'], csrf=False)
@http.route(_notify_url, type='json', 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:
data = json.loads(request.httprequest.data)
except (json.JSONDecodeError, TypeError, UnicodeDecodeError):
_logger.warning("BTCPay: received malformed JSON notification data")
return ''
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)
provider = tx_sudo.provider_id
client = BTCPayClient(host=provider.btcpay_location, pem=provider.btcpay_privateKey,
tokens={provider.btcpay_facade: provider.btcpay_token})
try:
ipn_data = data.get('data') if isinstance(data, dict) else None
if not isinstance(ipn_data, dict):
raise ValidationError(_("BTCPay: Invalid notification data."))
incoming_invoice_id = ipn_data.get('id')
incoming_order_id = ipn_data.get('orderId')
fetched_invoice = client.get_invoice(notification_data['invoiceID'])
_logger.info('fetched_invoice = %s',pprint.pformat(fetched_invoice))
# Never use the untrusted order ID to select a transaction. The
# stored invoice ID is the lookup key.
tx_sudo = self._find_transaction_by_invoice_id(incoming_invoice_id)
client = self._btcpay_client(tx_sudo.provider_id)
# Fetching through the authenticated API establishes the invoice's
# current state. Lock afterwards and re-check the stored ID so a
# concurrent checkout cannot replace it between lookup and use.
fetched_invoice = client.get_invoice(incoming_invoice_id)
self._lock_transaction(tx_sudo)
self._validate_invoice_binding(
tx_sudo, incoming_invoice_id, incoming_order_id, fetched_invoice,
)
_logger.info(
"BTCPay: verified invoice %s with status %s for transaction %s",
fetched_invoice.get('id'), fetched_invoice.get('status'), tx_sudo.reference,
)
notification_data = {
"reference": tx_sudo.reference,
"status": fetched_invoice.get('status'),
"invoiceID": incoming_invoice_id,
"txid": fetched_invoice.get('url'),
# Settled amount and currency, verified against the transaction
# before it is set done.
"amount": fetched_invoice.get('price'),
"currency": fetched_invoice.get('currency'),
}
notification_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 as error: # Acknowledge rejected notifications without retries.
_logger.warning("BTCPay: rejected notification: %s", error)
except ValidationError: # Acknowledge the notification to avoid getting spammed
_logger.exception("Unable to handle the notification data; skipping to acknowledge")
return ''
@http.route(_return_url, type='http', auth="public", methods=['GET'],
csrf=False, save_session=False)
@http.route(_return_url, type='http', auth="public", methods=['GET'], crsf=False, save_session=False)
def btcpay_return_from_redirect(self, **data):
""" BTCPay return
@@ -300,4 +109,4 @@ class BTCPayController(http.Controller):
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')
@@ -1,26 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="payment_method_btcpayserver" model="payment.method">
<field name="name">Pay with Bitcoin / Lightning Network</field>
<field name="code">btcpayserver</field>
<field name="active">True</field>
<field name="image" type="base64" file="payment_btcpayserver/static/description/icon.png"/>
<field name="support_tokenization">False</field>
<field name="support_express_checkout">False</field>
</record>
<record id="payment_provider_btcpayserver" model="payment.provider">
<field name="name">BTCPay Server payments</field>
<field name="name">BTCPay payments</field>
<field name="display_as">Pay with Bitcoin / Lightning Network</field>
<field name="code">btcpayserver</field>
<field name="image_128" type="base64" file="payment_btcpayserver/static/description/icon.png"/>
<field name="redirect_form_view_id" ref="redirect_form"/>
<field name="module_id" ref="base.module_payment_btcpayserver"/>
<field name="payment_method_ids"
eval="[Command.set([
ref('payment_btcpayserver.payment_method_btcpayserver'),
])]"
/>
<field name="redirect_form_view_id" ref="redirect_form"/>
</record>
</odoo>
@@ -1,6 +1,6 @@
import logging
from odoo import api, fields, models
from odoo import _, api, fields, models
from .libs.client import BTCPayClient
from .libs import crypto
@@ -20,9 +20,15 @@ class PaymentProvider(models.Model):
btcpay_privateKey = fields.Text(string='Private Key', help='Private Key for BTCPay Client. Leave empty, will be autogenerated during pairing.')
btcpay_facade = fields.Char(string='Facade', help='Token facade type: merchant/pos/payroll. Keep merchant', default='merchant')
def create(self, values_list):
if self.code == 'btcpayserver':
values_list['btcpay_privateKey'] = crypto.generate_privakey()
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 self.btcpay_pairingCode:
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)
@@ -1,9 +1,12 @@
import logging
import pprint
from werkzeug import urls
from odoo import _, fields, models
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.addons.payment import utils as payment_utils
_logger = logging.getLogger(__name__)
@@ -13,14 +16,12 @@ class PaymentTransaction(models.Model):
btcpay_invoiceId = fields.Char("Invoice Id")
btcpay_txid = fields.Char("Transaction Id")
btcpay_status = fields.Char("Transaction Status")
api_url = '/btcpay/checkout'
checkout_url = '/btcpay/checkout'
notify_url = 'payment/btcpay/ipn'
def _get_specific_rendering_values(self, processing_values):
""" Override of payment to return BTCPay-specific rendering values.
The redirect form only needs to carry the transaction reference: the
BTCPay invoice is built server-side (see the checkout controller) from
the transaction, so no amount, currency or buyer data is sent through
the browser.
""" Override of payment to return Specific rendering values.
Note: self.ensure_one() from `_get_processing_values`
@@ -28,13 +29,32 @@ 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 {
'api_url': '/btcpay/checkout',
'reference': self.reference,
'address1': self.partner_address,
'amount': self.amount,
'city': self.partner_city,
'country': self.partner_country_id.code,
'currency_code': self.currency_id.name,
'email': self.partner_email,
'first_name': partner_first_name,
'item_name': f"{self.company_id.name}: {self.reference}",
'item_number': self.reference,
'last_name': partner_last_name,
'lc': self.partner_lang,
'state': self.partner_state_id.name,
'zip_code': self.partner_zip,
'api_url': self.checkout_url,
'notify_url': base_url + self.notify_url,
}
def _get_tx_from_notification_data(self, provider_code, notification_data):
@@ -47,6 +67,7 @@ class PaymentTransaction(models.Model):
: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
@@ -58,13 +79,22 @@ class PaymentTransaction(models.Model):
)
return tx
def _handle_notification_data(self, provider_code, notification_data):
""" Match the transaction with the notification data, update its state and return it.
: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`
"""
tx = self._get_tx_from_notification_data(provider_code, notification_data)
tx._process_notification_data(notification_data)
tx._execute_callback()
return tx
def _process_notification_data(self, notification_data):
""" Override of payment to process the transaction based on BTCPay data.
Only the transaction state is updated here; confirming the related
sale order, creating the payment and sending the confirmation email are
handled by the generic post-processing of the `payment` framework.
Note: self.ensure_one()
:param dict notification_data: The notification data sent by the provider
@@ -75,69 +105,31 @@ class PaymentTransaction(models.Model):
if self.provider_code != 'btcpayserver':
return
_logger.info(
"processing notification data for BTCPay transaction with reference %s:\n%s",
self.reference, pprint.pformat(notification_data))
if not notification_data.get('reference'):
_logger.info("_process_notification_data %s", pprint.pformat(notification_data))
txn_id = notification_data.get('reference')
if not all(txn_id):
raise ValidationError(
"BTCPay: " + _("Received notification data with missing reference."))
"BTCPay: " + _("Missing value for txn_id (%(txn_id)s)).", txn_id=txn_id))
if notification_data.get('invoiceID'):
self.btcpay_invoiceId = notification_data['invoiceID']
self.provider_reference = notification_data['invoiceID']
if notification_data.get('txid'):
self.btcpay_txid = notification_data['txid']
status = notification_data.get('status')
self.btcpay_status = status
self.provider_reference = txn_id
self.btcpay_txid = notification_data.get('txid')
self.btcpay_status = notification_data.get('status')
if status in ('paid', 'processing'):
if self.btcpay_status in ['paid','processing']:
self._set_pending(state_message=notification_data.get('pending_reason'))
elif status in ('confirmed', 'complete'):
if self._btcpay_verify_amount(notification_data):
self._set_done()
elif status == 'new':
pass # Invoice created on BTCPay, waiting for the buyer to pay.
elif status in ('expired', 'cancel', 'cancelled'):
self._set_canceled(
state_message="BTCPay: " + _("Invoice status: %s.", status))
elif status == 'invalid':
_logger.warning(
"received invalid payment status for transaction with reference %s", self.reference)
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._set_canceled()
elif self.btcpay_status in ['invalid']:
_logger.info(
"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.", status))
else:
_logger.warning(
"received unrecognized payment status (%s) for transaction with reference %s",
status, self.reference)
self._set_error(
"BTCPay: " + _("Received data with unknown payment status: %s.", status))
def _btcpay_verify_amount(self, notification_data):
""" Check that the settled BTCPay invoice matches the transaction.
The BTCPay invoice is created from the transaction, but its settled
amount and currency are read back from BTCPay on notification and
compared here so that a tampered or mismatched invoice can never
confirm the transaction (and thus the order). On mismatch the
transaction is set in error and ``False`` is returned.
:param dict notification_data: The notification data sent by the provider
:return: Whether the settled amount and currency match the transaction
:rtype: bool
"""
self.ensure_one()
currency = notification_data.get('currency')
try:
amount = float(notification_data.get('amount'))
except (TypeError, ValueError):
amount = None
if (amount is None
or currency != self.currency_id.name
or self.currency_id.compare_amounts(amount, self.amount) != 0):
self._set_error("BTCPay: " + _(
"The settled amount (%(amount)s %(currency)s) does not match the "
"expected amount (%(expected)s %(expected_currency)s).",
amount=notification_data.get('amount'), currency=currency,
expected=self.amount, expected_currency=self.currency_id.name))
return False
return True
"BTCPay: " + _("Received data with invalid payment status: %s", self.btcpay_status)
)
+1 -1
View File
@@ -1 +1 @@
ecdsa
btcpay-python
Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

@@ -7,7 +7,7 @@
<h3 class="oe_slogan">This module allows you to accept Bitcoin / Lightning Network (and other cryptocurrency) payments in your Odoo e-commerce store.</h3>
<div class="oe_span12">
<div class="oe_bg_img">
<img src="BTCPay-Odoo-17-featured.png" class="oe_picture oe_screenshot">
<img src="BTCPay-Odoo-16-featured.png" class="oe_picture oe_screenshot">
</div>
</div>
</div>
@@ -65,11 +65,11 @@
<div class="oe_span12">
<p class="oe_mt32">
<ul>
<li>Odoo 17 running</li>
<li>Odoo 16 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>
<li><a href="https://docs.btcpayserver.org/CreateStore/" class="">You've a BTCPay store on the instance</a></li>
<li><a href="https://docs.btcpayserver.org/RegisterAccount/" class="">You've a registered account on the instance</a></li>
<li><a href="https://docs.btcpayserver.org/CreateStore/" class="">You've a BTCPay store on the instance</a></li>
<li><a href="https://docs.btcpayserver.org/WalletSetup/" class="">You've a wallet connected to your store</a></li>
</ul>
</p>
@@ -84,7 +84,7 @@
<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 `16.0` or downloaded a release tagged with version v16.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
@@ -152,7 +152,7 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan">How it looks like?</h2>
<h2 class="oe_slogan">How it looks like?</h2>
<div class="oe_span6">
<div class="oe_bg_img">
<img src="BTCPayLooksLike.png" class="oe_picture oe_screenshot">
@@ -171,7 +171,7 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan">Transaction BTCPay Details</h2>
<h2 class="oe_slogan">Transaction BTCPay Details</h2>
<div class="oe_span6">
<p class="oe_mt32">
<ul>
@@ -1,14 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- The redirect form is auto-submitted by the payment framework. It only
carries the transaction reference; the BTCPay invoice is built
server-side from the transaction (amount and currency cannot be
tampered with from the browser). -->
<template id="redirect_form">
<form t-att-action="api_url" method="post">
<input type="hidden" name="reference" t-att-value="reference"/>
<input type="hidden" name="reference" t-att-value="item_number"/>
<input type="hidden" name="name" t-att-value="first_name"/>
<input type="hidden" name="email" t-att-value="email"/>
<input type="hidden" name="street" t-att-value="address1"/>
<input type="hidden" name="zip" t-att-value="zip_code"/>
<input type="hidden" name="city" t-att-value="city"/>
<input type="hidden" name="currency_id" t-att-value="currency_code"/>
<input type="hidden" name="country" t-att-value="country"/>
<input type="hidden" name="custom" t-att-value="item_name"/>
<input type="hidden" name="amount" t-att-value="amount"/>
<input type="hidden" name="notify_url" t-att-value="notify_url"/>
<button type="submit" width="100px" t-att-class="submit_class">
<img t-if="not submit_txt" src="/static/description/icon.png"/>
<span t-if="submit_txt"> <t t-esc="submit_txt"/> <span class="fa fa-long-arrow-right"/>
</span>
</button>
</form>
</template>
</odoo>
</odoo>
@@ -7,11 +7,11 @@
<field name="inherit_id" ref="payment.payment_provider_form"/>
<field name="arch" type="xml">
<group name="provider_credentials" position='inside'>
<group invisible="code != 'btcpayserver'">
<group attrs="{'invisible': [('code', '!=', 'btcpayserver')]}">
<field name="btcpay_location"/>
<field name="btcpay_pairingCode"/>
</group>
<group invisible="code != 'btcpayserver'">
<group attrs="{'invisible': [('code', '!=', 'btcpayserver')]}">
<field name="btcpay_token"/>
<field name="btcpay_privateKey"/>
<field name="btcpay_facade"/>
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.