[REF] payment_odoo: migrate Odoo Payment to the new payment API
This commit also renames the module `payment_odoo_by_adyen` to
`payment_odoo` as well as the referring strings ("Odoo Payments" instead
of "Odoo Payments by Adyen", ...).
A dedicated [MOV] commit is not used because neither the [MOV] commit
nor the adapted [REF] would be valid on its own.
See the merge commit for more details.
task-2390665
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
|
||||
from odoo.addons.payment import reset_payment_acquirer
|
||||
from odoo.addons.payment.models.payment_acquirer import create_missing_journals # post-init hook
|
||||
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
reset_payment_acquirer(cr, registry, 'odoo')
|
||||
@@ -1,19 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Odoo Payments by Adyen Payment Acquirer',
|
||||
'name': 'Odoo Payments Payment Acquirer',
|
||||
'version': '2.0',
|
||||
'category': 'Accounting/Payment Acquirers',
|
||||
'sequence': 330,
|
||||
'summary': 'Payment Acquirer: Odoo Payments by Adyen',
|
||||
'version': '1.0',
|
||||
'description': """Odoo Payments by Adyen""",
|
||||
'summary': 'Payment Acquirer: Odoo Payments',
|
||||
'description': """Odoo Payments""",
|
||||
'depends': ['payment', 'adyen_platforms'],
|
||||
'data': [
|
||||
'views/payment_views.xml',
|
||||
'views/payment_odoo_by_adyen_templates.xml',
|
||||
'views/payment_odoo_templates.xml',
|
||||
'data/payment_acquirer_data.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'post_init_hook': 'create_missing_journals',
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# Adyen-specific mapping of currency codes in ISO 4217 format to the number of decimals.
|
||||
# Only currencies for which Adyen does not follow the ISO 4217 norm are listed here.
|
||||
# See https://docs.adyen.com/development-resources/currency-codes
|
||||
CURRENCY_DECIMALS = {
|
||||
'CLP': 2,
|
||||
'CVE': 0,
|
||||
'IDR': 0,
|
||||
'ISK': 2,
|
||||
}
|
||||
|
||||
# Mapping of transaction states to Adyen result codes.
|
||||
# See https://docs.adyen.com/checkout/payment-result-codes for the exhaustive list of result codes.
|
||||
RESULT_CODES_MAPPING = {
|
||||
'pending': (
|
||||
'ChallengeShopper', 'IdentifyShopper', 'Pending', 'PresentToShopper', 'Received',
|
||||
'RedirectShopper'
|
||||
),
|
||||
'done': ('Authorised',),
|
||||
'cancel': ('Cancelled',),
|
||||
}
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
@@ -0,0 +1,81 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import pprint
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
from odoo.addons.payment_odoo.const import CURRENCY_DECIMALS
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OdooController(http.Controller):
|
||||
_notification_url = '/payment/odoo/notification'
|
||||
|
||||
@http.route(_notification_url, type='json', auth='public')
|
||||
def odoo_notification(self):
|
||||
""" Process the data sent by Adyen to the webhook based on the event code.
|
||||
|
||||
See https://docs.adyen.com/development-resources/webhooks/understand-notifications for the
|
||||
exhaustive list of event codes.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
# Payload data represent a single notification's data. Because two notifications of a same
|
||||
# batch can be related to different sub-merchants, the proxy splits the batches and send
|
||||
# individual notifications one by one to this endpoint.
|
||||
notification_data = json.loads(request.httprequest.data)
|
||||
|
||||
# Check the source and integrity of the notification
|
||||
received_signature = notification_data.get('additionalData', {}).get(
|
||||
'metadata.merchant_signature'
|
||||
)
|
||||
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data(
|
||||
'odoo', notification_data
|
||||
)
|
||||
if not self._verify_notification_signature(received_signature, tx_sudo):
|
||||
return
|
||||
|
||||
_logger.info("notification received:\n%s", pprint.pformat(notification_data))
|
||||
if notification_data['success'] != 'true':
|
||||
return # Don't handle failed events
|
||||
|
||||
# Reshape the notification data for parsing
|
||||
event_code = notification_data['eventCode']
|
||||
if event_code == 'AUTHORISATION':
|
||||
notification_data['resultCode'] = 'Authorised'
|
||||
elif event_code == 'CANCELLATION':
|
||||
notification_data['resultCode'] = 'Cancelled'
|
||||
else:
|
||||
return # Don't handle unsupported event codes
|
||||
|
||||
# Handle the notification data as a regular feedback
|
||||
request.env['payment.transaction'].sudo()._handle_feedback_data('odoo', notification_data)
|
||||
|
||||
def _verify_notification_signature(self, received_signature, tx):
|
||||
""" Check that the signature computed from the transaction values matches the received one.
|
||||
|
||||
:param str received_signature: The signature sent with the notification
|
||||
:param recordset tx: The transaction of the notification, as a `payment.transaction` record
|
||||
:return: Whether the signatures match
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
if not received_signature:
|
||||
_logger.warning("ignored notification with missing signature")
|
||||
return False
|
||||
|
||||
converted_amount = payment_utils.to_minor_currency_units(
|
||||
tx.amount, tx.currency_id, CURRENCY_DECIMALS.get(tx.currency_id.name)
|
||||
)
|
||||
if not payment_utils.check_access_token(
|
||||
received_signature, converted_amount, tx.currency_id.name, tx.reference
|
||||
):
|
||||
_logger.warning("ignored notification with invalid signature")
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="payment.payment_acquirer_odoo" model="payment.acquirer">
|
||||
<field name="provider">odoo</field>
|
||||
<field name="redirect_form_view_id" ref="redirect_form"/>
|
||||
<field name="support_authorization">False</field>
|
||||
<field name="support_fees_computation">False</field>
|
||||
<field name="support_tokenization">True</field>
|
||||
<field name="allow_tokenization">True</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,5 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import payment_acquirer
|
||||
from . import payment_token
|
||||
from . import payment_transaction
|
||||
@@ -0,0 +1,42 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class PaymentAcquirer(models.Model):
|
||||
_inherit = 'payment.acquirer'
|
||||
|
||||
provider = fields.Selection(
|
||||
selection_add=[('odoo', "Odoo Payments")], ondelete={'odoo': 'set default'})
|
||||
odoo_adyen_account_id = fields.Many2one(
|
||||
related='company_id.adyen_account_id', required_if_provider='odoo')
|
||||
odoo_adyen_payout_id = fields.Many2one(
|
||||
string="Adyen Payout", comodel_name='adyen.payout', required_if_provider='odoo',
|
||||
domain='[("adyen_account_id", "=", odoo_adyen_account_id)]')
|
||||
|
||||
@api.constrains('provider', 'state')
|
||||
def _check_state_is_not_test(self):
|
||||
if any(a.provider == 'odoo' and a.state == 'test' for a in self):
|
||||
raise ValidationError(_("Odoo Payments is not available in test mode."))
|
||||
|
||||
def odoo_create_adyen_account(self):
|
||||
return self.env['adyen.account'].action_create_redirect()
|
||||
|
||||
def _odoo_get_api_url(self):
|
||||
self.ensure_one()
|
||||
proxy_url = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.proxy_url')
|
||||
return urls.url_join(proxy_url, 'pay_by_link')
|
||||
|
||||
def _odoo_compute_shopper_reference(self, partner_id):
|
||||
""" Compute a unique reference of the partner for Adyen.
|
||||
|
||||
This is used for the `shopperReference` field in communications with Adyen.
|
||||
|
||||
:param recordset partner_id: The partner making the transaction, as a `res.partner` id
|
||||
:return: The unique reference for the partner
|
||||
:rtype: str
|
||||
"""
|
||||
return f'{self.odoo_adyen_account_id.adyen_uuid}_{partner_id}'
|
||||
@@ -0,0 +1,9 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class PaymentToken(models.Model):
|
||||
_inherit = 'payment.token'
|
||||
|
||||
odoo_payment_method_type = fields.Char(string="Payment Method Type")
|
||||
@@ -0,0 +1,231 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import pprint
|
||||
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
from odoo.addons.payment_odoo.const import CURRENCY_DECIMALS, RESULT_CODES_MAPPING
|
||||
from odoo.addons.payment_odoo.controllers.main import OdooController
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentTransaction(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
def _get_specific_rendering_values(self, processing_values):
|
||||
""" Override of payment to return Odoo-specific rendering values.
|
||||
|
||||
Note: self.ensure_one() from `_get_processing_values`
|
||||
|
||||
:param dict processing_values: The generic and specific processing values of the transaction
|
||||
:return: The dict of acquirer-specific processing values
|
||||
:rtype: dict
|
||||
"""
|
||||
res = super()._get_specific_rendering_values(processing_values)
|
||||
if self.provider != 'odoo':
|
||||
return res
|
||||
|
||||
converted_amount = payment_utils.to_minor_currency_units(
|
||||
self.amount, self.currency_id, CURRENCY_DECIMALS.get(self.currency_id.name)
|
||||
)
|
||||
# The lang is taken from the context rather than from the partner because it is not required
|
||||
# to be logged to make a payment and because the lang is not always set on the partner.
|
||||
# Adyen only supports a reduced set of languages but, instead of looking for the closest
|
||||
# match in https://docs.adyen.com/checkout/components-web/localization-components, we simply
|
||||
# provide the lang string as is (after adapting the format) and let Adyen find the best fit.
|
||||
lang_code = (self._context.get('lang') or 'en-US').replace('_', '-')
|
||||
base_url = self.acquirer_id._get_base_url()
|
||||
signature = payment_utils.generate_access_token(
|
||||
converted_amount, self.currency_id.name, self.reference
|
||||
)
|
||||
data = {
|
||||
'adyen_uuid': self.acquirer_id.odoo_adyen_account_id.adyen_uuid,
|
||||
'payout': self.acquirer_id.odoo_adyen_payout_id.code,
|
||||
'amount': {
|
||||
'value': converted_amount,
|
||||
'currency': self.currency_id.name,
|
||||
},
|
||||
'reference': self.reference,
|
||||
'shopperLocale': lang_code,
|
||||
'shopperReference': self.acquirer_id._odoo_compute_shopper_reference(
|
||||
self.partner_id.id
|
||||
),
|
||||
'recurringProcessingModel': 'CardOnFile',
|
||||
'storePaymentMethod': self.tokenize, # True by default on Adyen side
|
||||
# Since the Pay by Link API redirects the customer without any payload, we use the
|
||||
# /payment/status route directly as return url.
|
||||
'returnUrl': urls.url_join(base_url, '/payment/status'),
|
||||
'metadata': {
|
||||
'merchant_signature': signature,
|
||||
'notification_url': urls.url_join(base_url, OdooController._notification_url),
|
||||
}, # Proxy-specific data
|
||||
}
|
||||
return {
|
||||
'data': json.dumps(data),
|
||||
'api_url': self.acquirer_id._odoo_get_api_url(),
|
||||
}
|
||||
|
||||
def _send_payment_request(self):
|
||||
""" Override of payment to send a payment request to Adyen through the Odoo proxy.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:return: None
|
||||
:raise: UserError if the transaction is not linked to a token
|
||||
"""
|
||||
super()._send_payment_request()
|
||||
if self.provider != 'odoo':
|
||||
return
|
||||
|
||||
# Make the payment request
|
||||
if not self.token_id:
|
||||
raise UserError("Odoo Payments: " + _("The transaction is not linked to a token."))
|
||||
|
||||
converted_amount = payment_utils.to_minor_currency_units(
|
||||
self.amount, self.currency_id, CURRENCY_DECIMALS.get(self.currency_id.name)
|
||||
)
|
||||
base_url = self.acquirer_id._get_base_url()
|
||||
signature = payment_utils.generate_access_token(
|
||||
converted_amount, self.currency_id.name, self.reference
|
||||
)
|
||||
data = {
|
||||
'payout': self.acquirer_id.odoo_adyen_payout_id.code,
|
||||
'amount': {
|
||||
'value': converted_amount,
|
||||
'currency': self.currency_id.name,
|
||||
},
|
||||
'reference': self.reference,
|
||||
'paymentMethod': {
|
||||
'type': self.token_id.odoo_payment_method_type,
|
||||
'storedPaymentMethodId': self.token_id.acquirer_ref,
|
||||
},
|
||||
'shopperReference': self.acquirer_id._odoo_compute_shopper_reference(
|
||||
self.partner_id.id
|
||||
),
|
||||
'recurringProcessingModel': 'Subscription',
|
||||
'shopperInteraction': 'ContAuth',
|
||||
'metadata': {
|
||||
'merchant_signature': signature,
|
||||
'notification_url': urls.url_join(base_url, OdooController._notification_url),
|
||||
}, # Proxy-specific data
|
||||
}
|
||||
response_content = self.acquirer_id.odoo_adyen_account_id._adyen_rpc('payments', data)
|
||||
|
||||
# Handle the payment request response
|
||||
_logger.info("payment request response:\n%s", pprint.pformat(response_content))
|
||||
self._handle_feedback_data('odoo', response_content)
|
||||
|
||||
@api.model
|
||||
def _get_tx_from_feedback_data(self, provider, data):
|
||||
""" Override of payment to find the transaction based on Adyen data.
|
||||
|
||||
:param str provider: The provider of the acquirer that handled the transaction
|
||||
:param dict data: The feedback data sent by the provider
|
||||
:return: The transaction if found
|
||||
:rtype: recordset of `payment.transaction`
|
||||
:raise: ValidationError if inconsistent data were received
|
||||
:raise: ValidationError if the data match no transaction
|
||||
"""
|
||||
tx = super()._get_tx_from_feedback_data(provider, data)
|
||||
if provider != 'odoo':
|
||||
return tx
|
||||
|
||||
reference = data.get('merchantReference')
|
||||
if not reference:
|
||||
raise ValidationError(
|
||||
"Odoo Payments: " + _("Received data with missing merchant reference")
|
||||
)
|
||||
|
||||
tx = self.search([('reference', '=', reference), ('provider', '=', 'odoo')])
|
||||
if not tx:
|
||||
raise ValidationError(
|
||||
"Odoo Payments: " + _("No transaction found matching reference %s.", reference)
|
||||
)
|
||||
return tx
|
||||
|
||||
def _process_feedback_data(self, data):
|
||||
""" Override of payment to process the transaction based on Adyen data.
|
||||
|
||||
Since only webhook notifications send data, the parsing is exclusively done according to the
|
||||
structure of NotificationRequestItem object.
|
||||
See https://docs.adyen.com/development-resources/webhooks/notifications-api
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:param dict data: The feedback data sent by the provider
|
||||
:return: None
|
||||
:raise: ValidationError if inconsistent data were received
|
||||
"""
|
||||
super()._process_feedback_data(data)
|
||||
if self.provider != 'odoo':
|
||||
return
|
||||
|
||||
# Handle the acquirer reference
|
||||
if 'pspReference' in data:
|
||||
self.acquirer_reference = data.get('pspReference')
|
||||
|
||||
# Handle the payment state
|
||||
payment_state = data.get('resultCode')
|
||||
if not payment_state:
|
||||
raise ValidationError(
|
||||
"Odoo Payments: " + _("Received data with missing payment state.")
|
||||
)
|
||||
|
||||
if payment_state in RESULT_CODES_MAPPING['pending']:
|
||||
self._set_pending()
|
||||
elif payment_state in RESULT_CODES_MAPPING['done']:
|
||||
has_token_data = 'recurring.recurringDetailReference' in data.get('additionalData', {})
|
||||
if self.tokenize and has_token_data:
|
||||
self._odoo_tokenize_from_feedback_data(data)
|
||||
self._set_done()
|
||||
elif payment_state in RESULT_CODES_MAPPING['cancel']:
|
||||
self._set_canceled()
|
||||
else: # Classify unsupported payment state as `error` tx state
|
||||
_logger.info("received data with invalid payment state: %s", payment_state)
|
||||
self._set_error(
|
||||
"Odoo Payments: " + _("Received data with invalid payment state: %s", payment_state)
|
||||
)
|
||||
|
||||
def _odoo_tokenize_from_feedback_data(self, data):
|
||||
""" Create a new token based on the feedback data.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:param dict data: The feedback data sent by the provider
|
||||
:return: None
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Retrieve all stored payment methods for the customer from the API and match them with the
|
||||
# acquirer reference of the transaction to find its payment method
|
||||
response_content = self.acquirer_id.odoo_adyen_account_id._adyen_rpc(
|
||||
'payment_methods',
|
||||
dict(shopperReference=data['additionalData']['recurring.shopperReference']),
|
||||
)
|
||||
payment_methods = response_content['storedPaymentMethods']
|
||||
acquirer_ref = data['additionalData']['recurring.recurringDetailReference']
|
||||
payment_method_type = next(pm['type'] for pm in payment_methods if pm['id'] == acquirer_ref)
|
||||
|
||||
# Create the token
|
||||
token = self.env['payment.token'].create({
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'name': payment_utils.build_token_name(data['additionalData'].get('cardSummary')),
|
||||
'partner_id': self.partner_id.id,
|
||||
'acquirer_ref': acquirer_ref,
|
||||
'verified': True, # The payment is authorized, so the payment method is valid
|
||||
'odoo_payment_method_type': payment_method_type,
|
||||
})
|
||||
self.write({
|
||||
'token_id': token,
|
||||
'tokenize': False,
|
||||
})
|
||||
_logger.info(
|
||||
"created token with id %s for partner with id %s", token.id, self.partner_id.id
|
||||
)
|
||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="redirect_form">
|
||||
<form t-att-action="api_url" method="post">
|
||||
<input type="hidden" name="data" t-att-value="data"/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="payment_acquirer_form" model="ir.ui.view">
|
||||
<field name="name">Odoo Payments Acquirer Form</field>
|
||||
<field name="model">payment.acquirer</field>
|
||||
<field name="inherit_id" ref="payment.payment_acquirer_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//group[@name="acquirer"]' position='inside'>
|
||||
<group attrs="{'invisible': [('provider', '!=', 'odoo')]}">
|
||||
<button name="odoo_create_adyen_account"
|
||||
icon="fa-arrow-right"
|
||||
type="object"
|
||||
class="btn-link"
|
||||
string="Create an account in 1 minute"
|
||||
attrs="{'invisible': [('odoo_adyen_account_id', '!=', False)]}"/>
|
||||
<field name="odoo_adyen_account_id"
|
||||
readonly="1"
|
||||
attrs="{'invisible': [('odoo_adyen_account_id', '=', False)]}"/>
|
||||
<field name="odoo_adyen_payout_id"
|
||||
attrs="{'invisible': [('odoo_adyen_account_id', '=', False)], 'required':[('provider', '=', 'odoo'), ('state', '!=', 'disabled')]}"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,5 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -1,22 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import pprint
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OdooByAdyenController(http.Controller):
|
||||
_notification_url = '/payment/odoo_adyen/notification'
|
||||
|
||||
@http.route('/payment/odoo_adyen/notification', type='json', auth='public', csrf=False)
|
||||
def odoo_adyen_notification(self):
|
||||
data = json.loads(request.httprequest.data)
|
||||
_logger.info('Beginning Odoo by Adyen form_feedback with data %s', pprint.pformat(data))
|
||||
if data.get('authResult') not in ['CANCELLED']:
|
||||
request.env['payment.transaction'].sudo().form_feedback(data, 'odoo_adyen')
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="payment.payment_acquirer_odoo_by_adyen" model="payment.acquirer">
|
||||
<field name="name">Odoo Payments by Adyen</field>
|
||||
<field name="image_128" type="base64" file="payment_odoo_by_adyen/static/src/img/odoo_icon.png"/>
|
||||
<field name="provider">odoo_adyen</field>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
<field name="view_template_id" ref="odoo_by_adyen_form"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import payment
|
||||
@@ -1,185 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.payment_odoo_by_adyen.controllers.main import OdooByAdyenController
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AcquirerOdooByAdyen(models.Model):
|
||||
_inherit = 'payment.acquirer'
|
||||
|
||||
provider = fields.Selection(selection_add=[
|
||||
('odoo_adyen', 'Odoo Payments by Adyen')
|
||||
], ondelete={'odoo_adyen': 'set default'})
|
||||
odoo_adyen_account_id = fields.Many2one('adyen.account', required_if_provider='odoo_adyen', related='company_id.adyen_account_id')
|
||||
odoo_adyen_payout_id = fields.Many2one('adyen.payout', required_if_provider='odoo_adyen', string='Adyen Payout', domain="[('adyen_account_id', '=', odoo_adyen_account_id)]")
|
||||
|
||||
@api.constrains('provider', 'state')
|
||||
def _check_odoo_adyen_test(self):
|
||||
for payment_acquirer in self:
|
||||
if payment_acquirer.provider == 'odoo_adyen' and payment_acquirer.state == 'test':
|
||||
raise ValidationError(_('Odoo Payments by Adyen is not available in test mode.'))
|
||||
|
||||
def _get_feature_support(self):
|
||||
res = super(AcquirerOdooByAdyen, self)._get_feature_support()
|
||||
res['tokenize'].append('odoo_adyen')
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _odoo_adyen_format_amount(self, amount, currency_id):
|
||||
return {
|
||||
'value': int(amount * (10 ** currency_id.decimal_places)),
|
||||
'currency': currency_id.name,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _odoo_adyen_compute_signature(self, amount, currency_id, reference):
|
||||
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
|
||||
token_str = '%s%s%s' % (
|
||||
int(amount * (10 ** currency_id.decimal_places)),
|
||||
currency_id.name,
|
||||
reference
|
||||
)
|
||||
return hmac.new(secret.encode('utf-8'), token_str.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
|
||||
def odoo_adyen_form_generate_values(self, values):
|
||||
# Don't use the value returned by `self.get_base_url` for the notification_url as
|
||||
# `request.httprequest.url_root` could be forged to retrieve the signature and
|
||||
# fake a payment update
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
data = {
|
||||
'adyen_uuid': self.odoo_adyen_account_id.adyen_uuid,
|
||||
'payout': self.odoo_adyen_payout_id.code,
|
||||
'amount': self._odoo_adyen_format_amount(values['amount'], values['currency']),
|
||||
'reference': values['reference'],
|
||||
'shopperLocale': values.get('partner_lang'),
|
||||
'metadata': {
|
||||
'merchant_signature': self._odoo_adyen_compute_signature(values['amount'],values['currency'],values['reference']),
|
||||
'notification_url': urls.url_join(base_url, OdooByAdyenController._notification_url),
|
||||
},
|
||||
'returnUrl': urls.url_join(self.get_base_url(), '/payment/process'),
|
||||
}
|
||||
|
||||
if self.save_token in ['ask', 'always']:
|
||||
data.update({
|
||||
'shopperReference': '%s_%s' % (self.odoo_adyen_account_id.adyen_uuid, values['partner_id']),
|
||||
'storePaymentMethod': True,
|
||||
'recurringProcessingModel': 'CardOnFile',
|
||||
})
|
||||
|
||||
values.update({
|
||||
'data': json.dumps(data),
|
||||
})
|
||||
return values
|
||||
|
||||
def odoo_adyen_get_form_action_url(self):
|
||||
self.ensure_one()
|
||||
proxy_url = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.proxy_url')
|
||||
return urls.url_join(proxy_url, 'pay_by_link')
|
||||
|
||||
def odoo_adyen_create_account(self):
|
||||
return self.env['adyen.account'].action_create_redirect()
|
||||
|
||||
class TxOdooByAdyen(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
def odoo_adyen_s2s_do_transaction(self, **kwargs):
|
||||
self.ensure_one()
|
||||
# Don't use the value returned by `self.get_base_url` for the notification_url as
|
||||
# `request.httprequest.url_root` could be forged to retrieve the signature and
|
||||
# fake a payment update
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
data = {
|
||||
'payout': self.acquirer_id.odoo_adyen_payout_id.code,
|
||||
'amount': self.acquirer_id._odoo_adyen_format_amount(self.amount, self.currency_id),
|
||||
'reference': self.reference,
|
||||
'paymentMethod': {
|
||||
'type': self.payment_token_id.odoo_adyen_payment_method_type,
|
||||
'storedPaymentMethodId': self.payment_token_id.acquirer_ref,
|
||||
},
|
||||
'shopperReference': '%s_%s' % (self.acquirer_id.odoo_adyen_account_id.adyen_uuid, self.partner_id.id),
|
||||
'shopperInteraction': 'ContAuth',
|
||||
'metadata': {
|
||||
'merchant_signature': self.acquirer_id._odoo_adyen_compute_signature(self.amount, self.currency_id, self.reference),
|
||||
'notification_url': urls.url_join(base_url, OdooByAdyenController._notification_url),
|
||||
},
|
||||
'returnUrl': urls.url_join(self.get_base_url(), '/payment/process'),
|
||||
}
|
||||
self.acquirer_id.odoo_adyen_account_id._adyen_rpc('payments', data)
|
||||
|
||||
@api.model
|
||||
def _odoo_adyen_form_get_tx_from_data(self, data):
|
||||
reference = data.get('merchantReference')
|
||||
if not reference:
|
||||
error_msg = _('Odoo Payments by Adyen: received data with missing reference (%s)', reference)
|
||||
_logger.info(error_msg)
|
||||
raise ValidationError(error_msg)
|
||||
|
||||
tx = self.env['payment.transaction'].search([('reference', '=', reference)])
|
||||
if not tx or len(tx) > 1:
|
||||
error_msg = _('Odoo Payments by Adyen: received data for reference %s') % (reference)
|
||||
if not tx:
|
||||
error_msg += _('; no order found')
|
||||
else:
|
||||
error_msg += _('; multiple order found')
|
||||
_logger.info(error_msg)
|
||||
raise ValidationError(error_msg)
|
||||
|
||||
return tx
|
||||
|
||||
def _odoo_adyen_form_get_invalid_parameters(self, data):
|
||||
invalid_parameters = []
|
||||
|
||||
if self.acquirer_reference and data.get('pspReference') != self.acquirer_reference:
|
||||
invalid_parameters.append(('pspReference', data.get('pspReference'), self.acquirer_reference))
|
||||
|
||||
return invalid_parameters
|
||||
|
||||
def _odoo_adyen_form_validate(self, data):
|
||||
merchant_signature = self.acquirer_id._odoo_adyen_compute_signature(self.amount, self.currency_id, self.reference)
|
||||
if merchant_signature != data['additionalData']['metadata.merchant_signature']:
|
||||
return False
|
||||
|
||||
# Save token
|
||||
if self.partner_id and not self.payment_token_id and \
|
||||
(self.type == 'form_save' or self.acquirer_id.save_token == 'always') \
|
||||
and 'recurring.shopperReference' in data['additionalData']:
|
||||
res = self.acquirer_id.odoo_adyen_account_id._adyen_rpc('payment_methods', {
|
||||
'shopperReference': data['additionalData']['recurring.shopperReference']
|
||||
})
|
||||
stored_payment_methods = res['storedPaymentMethods']
|
||||
pm_id = data['additionalData']['recurring.recurringDetailReference']
|
||||
token_id = self.env['payment.token'].create({
|
||||
'name': _("Card No XXXXXXXXXXXX%s", data['additionalData']['cardSummary']),
|
||||
'acquirer_ref': pm_id,
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
'odoo_adyen_payment_method_type': next(pm['type'] for pm in stored_payment_methods if pm['id'] == pm_id)
|
||||
})
|
||||
self.payment_token_id = token_id
|
||||
|
||||
# Update status
|
||||
if data['success']:
|
||||
self.write({'acquirer_reference': data.get('pspReference')})
|
||||
self._set_transaction_done()
|
||||
return True
|
||||
else:
|
||||
error = _('Odoo Payment by Adyen: feedback error')
|
||||
_logger.info(error)
|
||||
self.write({'state_message': error})
|
||||
self._set_transaction_cancel()
|
||||
return False
|
||||
|
||||
class PaymentToken(models.Model):
|
||||
_inherit = 'payment.token'
|
||||
|
||||
odoo_adyen_payment_method_type = fields.Char(string='PaymentMethod Type')
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<template id="odoo_by_adyen_form">
|
||||
<input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/>
|
||||
<input type="hidden" name="data" t-att-value="data"/>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="acquirer_form_odoo_by_adyen" model="ir.ui.view">
|
||||
<field name="name">acquirer.form.odoo.by.adyen</field>
|
||||
<field name="model">payment.acquirer</field>
|
||||
<field name="inherit_id" ref="payment.acquirer_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//group[@name="acquirer"]' position='inside'>
|
||||
<group attrs="{'invisible': [('provider', '!=', 'odoo_adyen')]}">
|
||||
<button name="odoo_adyen_create_account" icon="fa-arrow-right" type="object" class="btn-link" string="Create an account in 1 minute"
|
||||
attrs="{'invisible': [('odoo_adyen_account_id', '!=', False)]}"/>
|
||||
<field name="odoo_adyen_account_id" readonly="1" attrs="{'invisible': [('odoo_adyen_account_id', '=', False)]}"/>
|
||||
<field name="odoo_adyen_payout_id" attrs="{'invisible': [('odoo_adyen_account_id', '=', False)], 'required':[('provider', '=', 'odoo_adyen'), ('state', '!=', 'disabled')]}"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user