[REF] payment_paypal: migrate PayPal to the new payment API
See the merge commit for more details. task-2333036 Co-authored-by: Antoine Vandevenne <anv@odoo.com> Co-authored-by: Victor Feyens <vfe@odoo.com>
This commit is contained in:
committed by
Antoine Vandevenne (anv)
co-authored by
Antoine Vandevenne
Victor Feyens
parent
61a02a7330
commit
7b165cd555
@@ -1,10 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import controllers
|
||||
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
|
||||
from odoo.addons.payment import reset_payment_provider
|
||||
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_provider(cr, registry, 'paypal')
|
||||
reset_payment_acquirer(cr, registry, 'paypal')
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Paypal Payment Acquirer',
|
||||
'version': '2.0',
|
||||
'category': 'Accounting/Payment Acquirers',
|
||||
'sequence': 365,
|
||||
'summary': 'Payment Acquirer: Paypal Implementation',
|
||||
'version': '1.0',
|
||||
'description': """Paypal Payment Acquirer""",
|
||||
'depends': ['payment'],
|
||||
'data': [
|
||||
@@ -14,8 +14,7 @@
|
||||
'data/payment_acquirer_data.xml',
|
||||
'data/payment_paypal_email_data.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'post_init_hook': 'create_missing_journal_for_acquirers',
|
||||
'post_init_hook': 'create_missing_journals',
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
|
||||
@@ -1,119 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import pprint
|
||||
|
||||
import requests
|
||||
import werkzeug
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import http
|
||||
from odoo.addons.payment.models.payment_acquirer import ValidationError
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaypalController(http.Controller):
|
||||
_notify_url = '/payment/paypal/ipn/'
|
||||
_return_url = '/payment/paypal/dpn/'
|
||||
_cancel_url = '/payment/paypal/cancel/'
|
||||
_notify_url = '/payment/paypal/ipn/'
|
||||
|
||||
def _parse_pdt_response(self, response):
|
||||
""" Parse a text response for a PDT verification.
|
||||
@http.route(_return_url, type='http', auth='public', methods=['POST'], csrf=False)
|
||||
def paypal_dpn(self, **data):
|
||||
""" Route used by the PDT notification.
|
||||
|
||||
:param str response: text response, structured in the following way:
|
||||
STATUS\nkey1=value1\nkey2=value2...\n
|
||||
or STATUS\nError message...\n
|
||||
:rtype tuple(str, dict)
|
||||
:return: tuple containing the STATUS str and the key/value pairs
|
||||
parsed as a dict
|
||||
The "PDT notification" is actually POST data sent along the user redirection.
|
||||
"""
|
||||
lines = [line for line in response.split('\n') if line]
|
||||
status = lines.pop(0)
|
||||
_logger.info("beginning DPN with post data:\n%s", pprint.pformat(data))
|
||||
self._validate_data_authenticity(**data)
|
||||
request.env['payment.transaction']._handle_feedback_data('paypal', data)
|
||||
return werkzeug.utils.redirect('/payment/status')
|
||||
|
||||
pdt_post = {}
|
||||
for line in lines:
|
||||
split = line.split('=', 1)
|
||||
if len(split) == 2:
|
||||
pdt_post[split[0]] = urls.url_unquote_plus(split[1])
|
||||
else:
|
||||
_logger.warning('Paypal: error processing pdt response: %s', line)
|
||||
|
||||
return status, pdt_post
|
||||
|
||||
def paypal_validate_data(self, **post):
|
||||
""" Paypal IPN: three steps validation to ensure data correctness
|
||||
|
||||
- step 1: return an empty HTTP 200 response -> will be done at the end
|
||||
by returning ''
|
||||
- step 2: POST the complete, unaltered message back to Paypal (preceded
|
||||
by cmd=_notify-validate or _notify-synch for PDT), with same encoding
|
||||
- step 3: paypal send either VERIFIED or INVALID (single word) for IPN
|
||||
or SUCCESS or FAIL (+ data) for PDT
|
||||
|
||||
Once data is validated, process it. """
|
||||
res = False
|
||||
post['cmd'] = '_notify-validate'
|
||||
reference = post.get('item_number')
|
||||
tx = None
|
||||
if reference:
|
||||
tx = request.env['payment.transaction'].sudo().search([('reference', '=', reference)])
|
||||
if not tx:
|
||||
# we have seemingly received a notification for a payment that did not come from
|
||||
# odoo, acknowledge it otherwise paypal will keep trying
|
||||
_logger.warning('received notification for unknown payment reference')
|
||||
return False
|
||||
paypal_url = tx.acquirer_id.paypal_get_form_action_url()
|
||||
pdt_request = bool(post.get('amt')) # check for specific pdt param
|
||||
if pdt_request:
|
||||
# this means we are in PDT instead of DPN like before
|
||||
# fetch the PDT token
|
||||
post['at'] = tx and tx.acquirer_id.paypal_pdt_token or ''
|
||||
post['cmd'] = '_notify-synch' # command is different in PDT than IPN/DPN
|
||||
urequest = requests.post(paypal_url, post)
|
||||
urequest.raise_for_status()
|
||||
resp = urequest.text
|
||||
if pdt_request:
|
||||
resp, post = self._parse_pdt_response(resp)
|
||||
if resp in ['VERIFIED', 'SUCCESS']:
|
||||
_logger.info('Paypal: validated data')
|
||||
res = request.env['payment.transaction'].sudo().form_feedback(post, 'paypal')
|
||||
if not res and tx:
|
||||
tx._set_transaction_error('Validation error occured. Please contact your administrator.')
|
||||
elif resp in ['INVALID', 'FAIL']:
|
||||
_logger.warning('Paypal: answered INVALID/FAIL on data verification')
|
||||
if tx:
|
||||
tx._set_transaction_error('Invalid response from Paypal. Please contact your administrator.')
|
||||
else:
|
||||
_logger.warning('Paypal: unrecognized paypal answer, received %s instead of VERIFIED/SUCCESS or INVALID/FAIL (validation: %s)' % (resp, 'PDT' if pdt_request else 'IPN/DPN'))
|
||||
if tx:
|
||||
tx._set_transaction_error('Unrecognized error from Paypal. Please contact your administrator.')
|
||||
return res
|
||||
|
||||
@http.route('/payment/paypal/ipn/', type='http', auth='public', methods=['POST'], csrf=False)
|
||||
def paypal_ipn(self, **post):
|
||||
""" Paypal IPN. """
|
||||
_logger.info('Beginning Paypal IPN form_feedback with post data %s', pprint.pformat(post)) # debug
|
||||
@http.route(_notify_url, type='http', auth='public', methods=['GET', 'POST'], csrf=False)
|
||||
def paypal_ipn(self, **data):
|
||||
""" Route used by the IPN. """
|
||||
_logger.info("beginning IPN with post data:\n%s", pprint.pformat(data))
|
||||
try:
|
||||
self.paypal_validate_data(**post)
|
||||
except ValidationError:
|
||||
_logger.exception('Unable to validate the Paypal payment')
|
||||
self._validate_data_authenticity(**data)
|
||||
request.env['payment.transaction']._handle_feedback_data('paypal', data)
|
||||
except ValidationError: # Acknowledge the notification to avoid getting spammed
|
||||
_logger.exception("unable to handle the IPN data; skipping to acknowledge the notif")
|
||||
return ''
|
||||
|
||||
@http.route('/payment/paypal/dpn', type='http', auth="public", methods=['POST', 'GET'], csrf=False)
|
||||
def paypal_dpn(self, **post):
|
||||
""" Paypal DPN """
|
||||
_logger.info('Beginning Paypal DPN form_feedback with post data %s', pprint.pformat(post)) # debug
|
||||
try:
|
||||
res = self.paypal_validate_data(**post)
|
||||
except ValidationError:
|
||||
_logger.exception('Unable to validate the Paypal payment')
|
||||
return werkzeug.utils.redirect('/payment/process')
|
||||
def _validate_data_authenticity(self, **data):
|
||||
""" Validate the authenticity of data received through DPN or IPN
|
||||
|
||||
@http.route('/payment/paypal/cancel', type='http', auth="public", csrf=False)
|
||||
def paypal_cancel(self, **post):
|
||||
""" When the user cancels its Paypal payment: GET on this route """
|
||||
_logger.info('Beginning Paypal cancel with post data %s', pprint.pformat(post)) # debug
|
||||
return werkzeug.utils.redirect('/payment/process')
|
||||
The verification is done in three steps:
|
||||
- 1: POST the complete, unaltered, message back to Paypal (preceded by
|
||||
`cmd=_notify-validate`), in the same encoding.
|
||||
- 2: PayPal sends back either 'VERIFIED' or 'INVALID'.
|
||||
- 3: Return an empty HTTP 200 response (done at the end of the route method).
|
||||
See https://developer.paypal.com/docs/api-basics/notifications/ipn/IPNIntro
|
||||
|
||||
As per https://developer.paypal.com/docs/api-basics/notifications/payment-data-transfer/,
|
||||
PDT notifications should be verified in a similar but different manner:
|
||||
- The transaction ID should be retrieved from the GET param `tx`.
|
||||
- The POST should use `_notify-synch` (as per previous versions of this method) as `cmd`,
|
||||
and only have as params the transaction ID and the PDT Identity Token (under the key
|
||||
`at`, as per previous versions of this method).
|
||||
- The payment data should be parsed from the response of the check request.
|
||||
In practice, however, the transaction ID is never given by PayPal and the documentation
|
||||
has no mention of `_notify_synch` nor `at`. Because of this, PDT cannot be verified as
|
||||
prescribed by the documentation.
|
||||
Nevertheless, previous versions of this method used a bad heuristic (assessing the presence
|
||||
of the optional, PDT-specific, param `amt`) to determine whether the notification was a PDT.
|
||||
Since PDT notifications have in practice always been successfully authenticated by using the
|
||||
IPN protocol, this method does explicitly that for both PDT and IPN.
|
||||
|
||||
:param dict data: The data whose authenticity to check
|
||||
:return: None
|
||||
:raise: ValidationError if the authenticity could not be verified
|
||||
"""
|
||||
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data(
|
||||
'paypal', data
|
||||
)
|
||||
acquirer_sudo = tx_sudo.acquirer_id
|
||||
|
||||
# Request PayPal for an authenticity check
|
||||
data['cmd'] = '_notify-validate'
|
||||
response = requests.post(acquirer_sudo._paypal_get_api_url(), data, timeout=60)
|
||||
response.raise_for_status()
|
||||
|
||||
# Inspect the response code and raise if not 'VERIFIED'.
|
||||
response_code = response.text
|
||||
if response_code == 'VERIFIED':
|
||||
_logger.info("authenticity of notification data verified")
|
||||
elif response_code == 'INVALID':
|
||||
raise ValidationError("PayPal: " + _("Notification data were not acknowledged."))
|
||||
else:
|
||||
raise ValidationError(
|
||||
"PayPal: " + _(
|
||||
"Received unrecognized authentication check response code: received %s, "
|
||||
"expected VERIFIED or INVALID.",
|
||||
response_code
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="payment.payment_acquirer_paypal" model="payment.acquirer">
|
||||
<field name="name">Paypal</field>
|
||||
<field name="image_128" type="base64" file="payment_paypal/static/src/img/paypal_icon.png"/>
|
||||
<field name="provider">paypal</field>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
<field name="view_template_id" ref="paypal_form"/>
|
||||
</record>
|
||||
<record id="payment.payment_acquirer_paypal" model="payment.acquirer">
|
||||
<field name="provider">paypal</field>
|
||||
<field name="redirect_form_view_id" ref="redirect_form"/>
|
||||
<field name="support_authorization">False</field>
|
||||
<field name="support_fees_computation">True</field>
|
||||
<field name="support_tokenization">False</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
|
||||
<template id="mail_template_paypal_invite_user_to_configure">
|
||||
<div>
|
||||
<p>
|
||||
@@ -9,7 +10,10 @@
|
||||
Kindly follow the instructions given by PayPal to create your account.<br/>
|
||||
Then, help us complete your Paypal credentials in Odoo.<br/><br/>
|
||||
</p>
|
||||
<a t-attf-href="/web#id=#{acquirer.id}&model=payment.acquirer&view_type=form" style="background-color: #875A7B; padding: 10px; text-decoration: none; color: #fff; border-radius: 5px; font-size: 12px;">Set Paypal credentials</a>
|
||||
<a t-attf-href="/web#id=#{acquirer.id}&model=payment.acquirer&view_type=form"
|
||||
style="background-color: #875A7B; padding: 10px; text-decoration: none; color: #fff; border-radius: 5px; font-size: 12px;">
|
||||
Set Paypal credentials
|
||||
</a>
|
||||
<p>
|
||||
<br/><br/>
|
||||
Thanks,<br/>
|
||||
@@ -17,4 +21,5 @@
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import payment
|
||||
from . import payment_acquirer
|
||||
from . import payment_transaction
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
# coding: utf-8
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import dateutil.parser
|
||||
import pytz
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.payment.models.payment_acquirer import ValidationError
|
||||
from odoo.addons.payment_paypal.controllers.main import PaypalController
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AcquirerPaypal(models.Model):
|
||||
_inherit = 'payment.acquirer'
|
||||
|
||||
provider = fields.Selection(selection_add=[
|
||||
('paypal', 'Paypal')
|
||||
], ondelete={'paypal': 'set default'})
|
||||
paypal_email_account = fields.Char('Email', required_if_provider='paypal', groups='base.group_user')
|
||||
paypal_seller_account = fields.Char(
|
||||
'Merchant Account ID', groups='base.group_user',
|
||||
help='The Merchant ID is used to ensure communications coming from Paypal are valid and secured.')
|
||||
paypal_use_ipn = fields.Boolean('Use IPN', default=True, help='Paypal Instant Payment Notification', groups='base.group_user')
|
||||
paypal_pdt_token = fields.Char(string='PDT Identity Token', help='Payment Data Transfer allows you to receive notification of successful payments as they are made.', groups='base.group_user')
|
||||
# Default paypal fees
|
||||
fees_dom_fixed = fields.Float(default=0.35)
|
||||
fees_dom_var = fields.Float(default=3.4)
|
||||
fees_int_fixed = fields.Float(default=0.35)
|
||||
fees_int_var = fields.Float(default=3.9)
|
||||
|
||||
def _get_feature_support(self):
|
||||
"""Get advanced feature support by provider.
|
||||
|
||||
Each provider should add its technical in the corresponding
|
||||
key for the following features:
|
||||
* fees: support payment fees computations
|
||||
* authorize: support authorizing payment (separates
|
||||
authorization and capture)
|
||||
* tokenize: support saving payment data in a payment.tokenize
|
||||
object
|
||||
"""
|
||||
res = super(AcquirerPaypal, self)._get_feature_support()
|
||||
res['fees'].append('paypal')
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_paypal_urls(self, environment):
|
||||
""" Paypal URLS """
|
||||
if environment == 'prod':
|
||||
return {
|
||||
'paypal_form_url': 'https://www.paypal.com/cgi-bin/webscr',
|
||||
'paypal_rest_url': 'https://api.paypal.com/v1/oauth2/token',
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'paypal_form_url': 'https://www.sandbox.paypal.com/cgi-bin/webscr',
|
||||
'paypal_rest_url': 'https://api.sandbox.paypal.com/v1/oauth2/token',
|
||||
}
|
||||
|
||||
def paypal_compute_fees(self, amount, currency_id, country_id):
|
||||
""" Compute paypal fees.
|
||||
|
||||
:param float amount: the amount to pay
|
||||
:param integer country_id: an ID of a res.country, or None. This is
|
||||
the customer's country, to be compared to
|
||||
the acquirer company country.
|
||||
:return float fees: computed fees
|
||||
"""
|
||||
if not self.fees_active:
|
||||
return 0.0
|
||||
country = self.env['res.country'].browse(country_id)
|
||||
if country and self.company_id.sudo().country_id.id == country.id:
|
||||
percentage = self.fees_dom_var
|
||||
fixed = self.fees_dom_fixed
|
||||
else:
|
||||
percentage = self.fees_int_var
|
||||
fixed = self.fees_int_fixed
|
||||
fees = (percentage / 100.0 * amount + fixed) / (1 - percentage / 100.0)
|
||||
return fees
|
||||
|
||||
def paypal_form_generate_values(self, values):
|
||||
base_url = self.get_base_url()
|
||||
|
||||
paypal_tx_values = dict(values)
|
||||
paypal_tx_values.update({
|
||||
'cmd': '_xclick',
|
||||
'business': self.paypal_email_account,
|
||||
'item_name': '%s: %s' % (self.company_id.name, values['reference']),
|
||||
'item_number': values['reference'],
|
||||
'amount': values['amount'],
|
||||
'currency_code': values['currency'] and values['currency'].name or '',
|
||||
'address1': values.get('partner_address'),
|
||||
'city': values.get('partner_city'),
|
||||
'country': values.get('partner_country') and values.get('partner_country').code or '',
|
||||
'state': values.get('partner_state') and (values.get('partner_state').code or values.get('partner_state').name) or '',
|
||||
'email': values.get('partner_email'),
|
||||
'zip_code': values.get('partner_zip'),
|
||||
'first_name': values.get('partner_first_name'),
|
||||
'last_name': values.get('partner_last_name'),
|
||||
'paypal_return': urls.url_join(base_url, PaypalController._return_url),
|
||||
'notify_url': urls.url_join(base_url, PaypalController._notify_url),
|
||||
'cancel_return': urls.url_join(base_url, PaypalController._cancel_url),
|
||||
'handling': '%.2f' % paypal_tx_values.pop('fees', 0.0) if self.fees_active else False,
|
||||
'custom': json.dumps({'return_url': '%s' % paypal_tx_values.pop('return_url')}) if paypal_tx_values.get('return_url') else False,
|
||||
})
|
||||
return paypal_tx_values
|
||||
|
||||
def paypal_get_form_action_url(self):
|
||||
self.ensure_one()
|
||||
environment = 'prod' if self.state == 'enabled' else 'test'
|
||||
return self._get_paypal_urls(environment)['paypal_form_url']
|
||||
|
||||
|
||||
class TxPaypal(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
paypal_txn_type = fields.Char('Transaction type')
|
||||
|
||||
# --------------------------------------------------
|
||||
# FORM RELATED METHODS
|
||||
# --------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _paypal_form_get_tx_from_data(self, data):
|
||||
reference, txn_id = data.get('item_number'), data.get('txn_id')
|
||||
if not reference or not txn_id:
|
||||
error_msg = _('Paypal: received data with missing reference (%s) or txn_id (%s)') % (reference, txn_id)
|
||||
_logger.info(error_msg)
|
||||
raise ValidationError(error_msg)
|
||||
|
||||
# find tx -> @TDENOTE use txn_id ?
|
||||
txs = self.env['payment.transaction'].search([('reference', '=', reference)])
|
||||
if not txs or len(txs) > 1:
|
||||
error_msg = 'Paypal: received data for reference %s' % (reference)
|
||||
if not txs:
|
||||
error_msg += '; no order found'
|
||||
else:
|
||||
error_msg += '; multiple order found'
|
||||
_logger.info(error_msg)
|
||||
raise ValidationError(error_msg)
|
||||
return txs[0]
|
||||
|
||||
def _paypal_form_get_invalid_parameters(self, data):
|
||||
invalid_parameters = []
|
||||
_logger.info('Received a notification from Paypal with IPN version %s', data.get('notify_version'))
|
||||
if data.get('test_ipn'):
|
||||
_logger.warning(
|
||||
'Received a notification from Paypal using sandbox'
|
||||
),
|
||||
|
||||
# TODO: txn_id: shoudl be false at draft, set afterwards, and verified with txn details
|
||||
if self.acquirer_reference and data.get('txn_id') != self.acquirer_reference:
|
||||
invalid_parameters.append(('txn_id', data.get('txn_id'), self.acquirer_reference))
|
||||
# check what is buyed
|
||||
if float_compare(float(data.get('mc_gross', '0.0')), (self.amount + self.fees), 2) != 0:
|
||||
invalid_parameters.append(('mc_gross', data.get('mc_gross'), '%.2f' % (self.amount + self.fees))) # mc_gross is amount + fees
|
||||
if data.get('mc_currency') != self.currency_id.name:
|
||||
invalid_parameters.append(('mc_currency', data.get('mc_currency'), self.currency_id.name))
|
||||
if 'handling_amount' in data and float_compare(float(data.get('handling_amount')), self.fees, 2) != 0:
|
||||
invalid_parameters.append(('handling_amount', data.get('handling_amount'), self.fees))
|
||||
# check buyer
|
||||
if self.payment_token_id and data.get('payer_id') != self.payment_token_id.acquirer_ref:
|
||||
invalid_parameters.append(('payer_id', data.get('payer_id'), self.payment_token_id.acquirer_ref))
|
||||
# check seller
|
||||
if data.get('receiver_id') and self.acquirer_id.paypal_seller_account and data['receiver_id'] != self.acquirer_id.paypal_seller_account:
|
||||
invalid_parameters.append(('receiver_id', data.get('receiver_id'), self.acquirer_id.paypal_seller_account))
|
||||
if not data.get('receiver_id') or not self.acquirer_id.paypal_seller_account:
|
||||
# Check receiver_email only if receiver_id was not checked.
|
||||
# In Paypal, this is possible to configure as receiver_email a different email than the business email (the login email)
|
||||
# In Odoo, there is only one field for the Paypal email: the business email. This isn't possible to set a receiver_email
|
||||
# different than the business email. Therefore, if you want such a configuration in your Paypal, you are then obliged to fill
|
||||
# the Merchant ID in the Paypal payment acquirer in Odoo, so the check is performed on this variable instead of the receiver_email.
|
||||
# At least one of the two checks must be done, to avoid fraudsters.
|
||||
if data.get('receiver_email') and data.get('receiver_email') != self.acquirer_id.paypal_email_account:
|
||||
invalid_parameters.append(('receiver_email', data.get('receiver_email'), self.acquirer_id.paypal_email_account))
|
||||
if data.get('business') and data.get('business') != self.acquirer_id.paypal_email_account:
|
||||
invalid_parameters.append(('business', data.get('business'), self.acquirer_id.paypal_email_account))
|
||||
|
||||
return invalid_parameters
|
||||
|
||||
def _paypal_form_validate(self, data):
|
||||
status = data.get('payment_status')
|
||||
former_tx_state = self.state
|
||||
res = {
|
||||
'acquirer_reference': data.get('txn_id'),
|
||||
'paypal_txn_type': data.get('payment_type'),
|
||||
}
|
||||
if not self.acquirer_id.paypal_pdt_token and not self.acquirer_id.paypal_seller_account and status in ['Completed', 'Processed', 'Pending']:
|
||||
template = self.env.ref('payment_paypal.mail_template_paypal_invite_user_to_configure', False)
|
||||
if template:
|
||||
render_template = template._render({
|
||||
'acquirer': self.acquirer_id,
|
||||
}, engine='ir.qweb')
|
||||
mail_body = self.env['mail.render.mixin']._replace_local_links(render_template)
|
||||
mail_values = {
|
||||
'body_html': mail_body,
|
||||
'subject': _('Add your Paypal account to Odoo'),
|
||||
'email_to': self.acquirer_id.paypal_email_account,
|
||||
'email_from': self.acquirer_id.create_uid.email_formatted,
|
||||
'author_id': self.acquirer_id.create_uid.partner_id.id,
|
||||
}
|
||||
self.env['mail.mail'].sudo().create(mail_values).send()
|
||||
|
||||
if status in ['Completed', 'Processed']:
|
||||
try:
|
||||
# dateutil and pytz don't recognize abbreviations PDT/PST
|
||||
tzinfos = {
|
||||
'PST': -8 * 3600,
|
||||
'PDT': -7 * 3600,
|
||||
}
|
||||
date = dateutil.parser.parse(data.get('payment_date'), tzinfos=tzinfos).astimezone(pytz.utc).replace(tzinfo=None)
|
||||
except:
|
||||
date = fields.Datetime.now()
|
||||
res.update(date=date)
|
||||
self._set_transaction_done()
|
||||
if self.state == 'done' and self.state != former_tx_state:
|
||||
_logger.info('Validated Paypal payment for tx %s: set as done' % (self.reference))
|
||||
return self.write(res)
|
||||
return True
|
||||
elif status in ['Pending', 'Expired']:
|
||||
res.update(state_message=data.get('pending_reason', ''))
|
||||
self._set_transaction_pending()
|
||||
if self.state == 'pending' and self.state != former_tx_state:
|
||||
_logger.info('Received notification for Paypal payment %s: set as pending' % (self.reference))
|
||||
return self.write(res)
|
||||
return True
|
||||
else:
|
||||
error = 'Received unrecognized status for Paypal payment %s: %s, set as error' % (self.reference, status)
|
||||
res.update(state_message=error)
|
||||
self._set_transaction_cancel()
|
||||
if self.state == 'cancel' and self.state != former_tx_state:
|
||||
_logger.info(error)
|
||||
return self.write(res)
|
||||
return True
|
||||
@@ -0,0 +1,54 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import _, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentAcquirer(models.Model):
|
||||
_inherit = 'payment.acquirer'
|
||||
|
||||
provider = fields.Selection(
|
||||
selection_add=[('paypal', "Paypal")], ondelete={'paypal': 'set default'})
|
||||
paypal_email_account = fields.Char(
|
||||
string="Email",
|
||||
help="The public business email solely used to identify the account with PayPal",
|
||||
required_if_provider='paypal')
|
||||
paypal_seller_account = fields.Char(
|
||||
string="Merchant Account ID", groups='base.group_system')
|
||||
paypal_pdt_token = fields.Char(string="PDT Identity Token", groups='base.group_system')
|
||||
paypal_use_ipn = fields.Boolean(
|
||||
string="Use IPN", help="Paypal Instant Payment Notification", default=True)
|
||||
|
||||
def _paypal_get_api_url(self):
|
||||
""" Return the API URL according to the acquirer state.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:return: The API URL
|
||||
:rtype: str
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if self.state == 'enabled':
|
||||
return 'https://www.paypal.com/cgi-bin/webscr'
|
||||
else:
|
||||
return 'https://www.sandbox.paypal.com/cgi-bin/webscr'
|
||||
|
||||
def _paypal_send_configuration_reminder(self):
|
||||
template = self.env.ref(
|
||||
'payment_paypal.mail_template_paypal_invite_user_to_configure', raise_if_not_found=False
|
||||
)
|
||||
if template:
|
||||
render_template = template._render({'acquirer': self}, engine='ir.qweb')
|
||||
mail_body = self.env['mail.render.mixin']._replace_local_links(render_template)
|
||||
mail_values = {
|
||||
'body_html': mail_body,
|
||||
'subject': _("Add your PayPal account to Odoo"),
|
||||
'email_to': self.paypal_email_account,
|
||||
'email_from': self.create_uid.email_formatted,
|
||||
'author_id': self.create_uid.partner_id.id,
|
||||
}
|
||||
self.env['mail.mail'].sudo().create(mail_values).send()
|
||||
@@ -0,0 +1,125 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
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_paypal.controllers.main import PaypalController
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentTransaction(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
# See https://developer.paypal.com/docs/api-basics/notifications/ipn/IPNandPDTVariables/
|
||||
paypal_type = fields.Char(
|
||||
string="PayPal Transaction Type", help="This has no use in Odoo except for debugging.")
|
||||
|
||||
def _get_specific_rendering_values(self, processing_values):
|
||||
""" Override of payment to return Paypal-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 != 'paypal':
|
||||
return res
|
||||
|
||||
base_url = self.acquirer_id._get_base_url()
|
||||
partner_first_name, partner_last_name = payment_utils.split_partner_name(self.partner_name)
|
||||
notify_url = self.acquirer_id.paypal_use_ipn \
|
||||
and urls.url_join(base_url, PaypalController._notify_url)
|
||||
return {
|
||||
'address1': self.partner_address,
|
||||
'amount': self.amount,
|
||||
'business': self.acquirer_id.paypal_email_account,
|
||||
'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,
|
||||
'handling': self.fees,
|
||||
'item_name': f"{self.company_id.name}: {self.reference}",
|
||||
'item_number': self.reference,
|
||||
'last_name': partner_last_name,
|
||||
'lc': self.partner_lang,
|
||||
'notify_url': notify_url,
|
||||
'return_url': urls.url_join(base_url, PaypalController._return_url),
|
||||
'state': self.partner_state_id.name,
|
||||
'zip_code': self.partner_zip,
|
||||
'api_url': self.acquirer_id._paypal_get_api_url(),
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_tx_from_feedback_data(self, provider, data):
|
||||
""" Override of payment to find the transaction based on Paypal 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 the data match no transaction
|
||||
"""
|
||||
tx = super()._get_tx_from_feedback_data(provider, data)
|
||||
if provider != 'paypal':
|
||||
return tx
|
||||
|
||||
reference = data.get('item_number')
|
||||
tx = self.search([('reference', '=', reference), ('provider', '=', 'paypal')])
|
||||
if not tx:
|
||||
raise ValidationError(
|
||||
"PayPal: " + _("No transaction found matching reference %s.", reference)
|
||||
)
|
||||
return tx
|
||||
|
||||
def _process_feedback_data(self, data):
|
||||
""" Override of payment to process the transaction based on Paypal data.
|
||||
|
||||
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 != 'paypal':
|
||||
return
|
||||
|
||||
txn_id = data.get('txn_id')
|
||||
txn_type = data.get('txn_type')
|
||||
if not all((txn_id, txn_type)):
|
||||
raise ValidationError(
|
||||
"PayPal: " + _(
|
||||
"Missing value for txn_id (%(txn_id)s) or txn_type (%(txn_type)s).",
|
||||
txn_id=txn_id, txn_type=txn_type
|
||||
)
|
||||
)
|
||||
self.acquirer_reference = txn_id
|
||||
self.paypal_type = txn_type
|
||||
|
||||
payment_status = data.get('payment_status')
|
||||
|
||||
if payment_status in ('Pending', 'Processed', 'Completed') and not all(
|
||||
(self.acquirer_id.paypal_pdt_token, self.acquirer_id.paypal_seller_account)
|
||||
): # If a payment is made on an account waiting for configuration, send a reminder email
|
||||
self.acquirer_id._paypal_send_configuration_reminder()
|
||||
|
||||
if payment_status in ('Processed', 'Completed'):
|
||||
self._set_done()
|
||||
elif payment_status == 'Pending':
|
||||
self._set_pending(state_message=data.get('pending_reason'))
|
||||
elif payment_status == 'Expired':
|
||||
self._set_canceled()
|
||||
else:
|
||||
_logger.info("received data with invalid payment status: %s", payment_status)
|
||||
self._set_error(
|
||||
"PayPal: " + _("Received data with invalid payment status: %s", payment_status)
|
||||
)
|
||||
@@ -1,2 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_paypal
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from . import test_paypal
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.addons.payment.tests.common import PaymentCommon
|
||||
|
||||
|
||||
class PaypalCommon(PaymentCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.paypal = cls._prepare_acquirer('paypal', update_values={
|
||||
'paypal_email_account': 'dummy@test.mail.com',
|
||||
'fees_active': False,
|
||||
})
|
||||
|
||||
# Override default values
|
||||
cls.acquirer = cls.paypal
|
||||
cls.currency = cls.currency_euro
|
||||
@@ -1,102 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import fields
|
||||
from odoo.addons.payment.models.payment_acquirer import ValidationError
|
||||
from odoo.addons.payment.tests.common import PaymentAcquirerCommon
|
||||
from odoo.addons.payment_paypal.controllers.main import PaypalController
|
||||
from werkzeug import urls
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tests import tagged
|
||||
|
||||
from lxml import objectify
|
||||
from .common import PaypalCommon
|
||||
from ..controllers.main import PaypalController
|
||||
|
||||
|
||||
class PaypalCommon(PaymentAcquirerCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.paypal = cls.env.ref('payment.payment_acquirer_paypal')
|
||||
cls.paypal.write({
|
||||
'paypal_email_account': 'dummy',
|
||||
'state': 'test',
|
||||
})
|
||||
|
||||
# some CC
|
||||
cls.amex = (('378282246310005', '123'), ('371449635398431', '123'))
|
||||
cls.amex_corporate = (('378734493671000', '123'))
|
||||
cls.autralian_bankcard = (('5610591081018250', '123'))
|
||||
cls.dinersclub = (('30569309025904', '123'), ('38520000023237', '123'))
|
||||
cls.discover = (('6011111111111117', '123'), ('6011000990139424', '123'))
|
||||
cls.jcb = (('3530111333300000', '123'), ('3566002020360505', '123'))
|
||||
cls.mastercard = (('5555555555554444', '123'), ('5105105105105100', '123'))
|
||||
cls.visa = (('4111111111111111', '123'), ('4012888888881881', '123'), ('4222222222222', '123'))
|
||||
cls.dankord_pbs = (('76009244561', '123'), ('5019717010103742', '123'))
|
||||
cls.switch_polo = (('6331101999990016', '123'))
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'external', '-standard')
|
||||
@tagged('post_install', '-at_install')
|
||||
class PaypalForm(PaypalCommon):
|
||||
|
||||
def test_10_paypal_form_render(self):
|
||||
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
|
||||
# be sure not to do stupid things
|
||||
self.paypal.write({'paypal_email_account': 'tde+paypal-facilitator@odoo.com', 'fees_active': False})
|
||||
self.assertEqual(self.paypal.state, 'test', 'test without test environment')
|
||||
|
||||
# ----------------------------------------
|
||||
# Test: button direct rendering
|
||||
# ----------------------------------------
|
||||
|
||||
# render the button
|
||||
res = self.paypal.render(
|
||||
'test_ref0', 0.01, self.currency_euro.id,
|
||||
values=self.buyer_values)
|
||||
|
||||
form_values = {
|
||||
'cmd': '_xclick',
|
||||
'business': 'tde+paypal-facilitator@odoo.com',
|
||||
'item_name': '%s: test_ref0' % (self.paypal.company_id.name),
|
||||
'item_number': 'test_ref0',
|
||||
'first_name': 'Norbert',
|
||||
'last_name': 'Buyer',
|
||||
'amount': '0.01',
|
||||
'bn': 'OdooInc_SP',
|
||||
'currency_code': 'EUR',
|
||||
def _get_expected_values(self):
|
||||
return_url = self._build_url(PaypalController._return_url)
|
||||
values = {
|
||||
'address1': 'Huge Street 2/543',
|
||||
'amount': str(self.amount),
|
||||
'business': self.paypal.paypal_email_account,
|
||||
'cancel_return': return_url,
|
||||
'city': 'Sin City',
|
||||
'zip': '1000',
|
||||
'rm': '2',
|
||||
'cmd': '_xclick',
|
||||
'country': 'BE',
|
||||
'currency_code': self.currency.name,
|
||||
'email': 'norbert.buyer@example.com',
|
||||
'return': urls.url_join(base_url, PaypalController._return_url),
|
||||
'notify_url': urls.url_join(base_url, PaypalController._notify_url),
|
||||
'cancel_return': urls.url_join(base_url, PaypalController._cancel_url),
|
||||
'custom': '{"return_url": "/payment/process"}',
|
||||
'first_name': 'Norbert',
|
||||
'item_name': f'{self.paypal.company_id.name}: {self.reference}',
|
||||
'item_number': self.reference,
|
||||
'last_name': 'Buyer',
|
||||
'lc': 'en_US',
|
||||
'notify_url': self._build_url(PaypalController._notify_url),
|
||||
'return': return_url,
|
||||
'rm': '2',
|
||||
'zip': '1000',
|
||||
}
|
||||
|
||||
# check form result
|
||||
tree = objectify.fromstring(res)
|
||||
if self.paypal.fees_active:
|
||||
fees = self.currency.round(self.paypal._compute_fees(self.amount, self.currency, self.partner.country_id))
|
||||
if fees:
|
||||
# handling input is only specified if truthy
|
||||
values['handling'] = str(fees)
|
||||
|
||||
data_set = tree.xpath("//input[@name='data_set']")
|
||||
self.assertEqual(len(data_set), 1, 'paypal: Found %d "data_set" input instead of 1' % len(data_set))
|
||||
self.assertEqual(data_set[0].get('data-action-url'), 'https://www.sandbox.paypal.com/cgi-bin/webscr', 'paypal: wrong form POST url')
|
||||
for form_input in tree.input:
|
||||
if form_input.get('name') in ['submit', 'data_set']:
|
||||
continue
|
||||
self.assertEqual(
|
||||
form_input.get('value'),
|
||||
form_values[form_input.get('name')],
|
||||
'paypal: wrong value for input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')])
|
||||
)
|
||||
return values
|
||||
|
||||
def test_11_paypal_form_with_fees(self):
|
||||
# be sure not to do stupid things
|
||||
self.assertEqual(self.paypal.state, 'test', 'test without test environment')
|
||||
def test_redirect_form_values(self):
|
||||
tx = self.create_transaction(flow='redirect')
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = tx._get_processing_values()
|
||||
|
||||
# update acquirer: compute fees
|
||||
form_info = self._extract_values_from_html_form(processing_values['redirect_form_html'])
|
||||
self.assertEqual(
|
||||
form_info['action'],
|
||||
'https://www.sandbox.paypal.com/cgi-bin/webscr')
|
||||
|
||||
expected_values = self._get_expected_values()
|
||||
self.assertDictEqual(
|
||||
expected_values, form_info['inputs'],
|
||||
"Paypal: invalid inputs specified in the redirect form.")
|
||||
|
||||
def test_redirect_form_with_fees(self):
|
||||
self.paypal.write({
|
||||
'fees_active': True,
|
||||
'fees_dom_fixed': 1.0,
|
||||
@@ -104,30 +65,19 @@ class PaypalForm(PaypalCommon):
|
||||
'fees_int_fixed': 1.5,
|
||||
'fees_int_var': 0.50,
|
||||
})
|
||||
expected_values = self._get_expected_values()
|
||||
|
||||
# render the button
|
||||
res = self.paypal.render(
|
||||
'test_ref0', 12.50, self.currency_euro.id,
|
||||
values=self.buyer_values)
|
||||
tx = self.create_transaction(flow='redirect')
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = tx._get_processing_values()
|
||||
form_info = self._extract_values_from_html_form(processing_values['redirect_form_html'])
|
||||
|
||||
# check form result
|
||||
handling_found = False
|
||||
tree = objectify.fromstring(res)
|
||||
|
||||
data_set = tree.xpath("//input[@name='data_set']")
|
||||
self.assertEqual(len(data_set), 1, 'paypal: Found %d "data_set" input instead of 1' % len(data_set))
|
||||
self.assertEqual(data_set[0].get('data-action-url'), 'https://www.sandbox.paypal.com/cgi-bin/webscr', 'paypal: wrong form POST url')
|
||||
for form_input in tree.input:
|
||||
if form_input.get('name') in ['handling']:
|
||||
handling_found = True
|
||||
self.assertEqual(form_input.get('value'), '1.57', 'paypal: wrong computed fees')
|
||||
self.assertTrue(handling_found, 'paypal: fees_active did not add handling input in rendered form')
|
||||
|
||||
@mute_logger('odoo.addons.payment_paypal.models.payment', 'ValidationError')
|
||||
def test_20_paypal_form_management(self):
|
||||
# be sure not to do stupid things
|
||||
self.assertEqual(self.paypal.state, 'test', 'test without test environment')
|
||||
self.assertEqual(form_info['action'], 'https://www.sandbox.paypal.com/cgi-bin/webscr')
|
||||
self.assertDictEqual(
|
||||
expected_values, form_info['inputs'],
|
||||
"Paypal: invalid inputs specified in the redirect form.")
|
||||
|
||||
def test_feedback_processing(self):
|
||||
# typical data posted by paypal after client has successfully paid
|
||||
paypal_post_data = {
|
||||
'protection_eligibility': u'Ineligible',
|
||||
@@ -145,14 +95,13 @@ class PaypalForm(PaypalCommon):
|
||||
'handling_amount': u'0.00',
|
||||
'payment_date': u'03:21:19 Nov 18, 2013 PST',
|
||||
'first_name': u'Norbert',
|
||||
'item_name': u'test_ref_2',
|
||||
'item_name': self.reference,
|
||||
'address_country': u'France',
|
||||
'charset': u'windows-1252',
|
||||
'custom': u'{"return_url": "/payment/process"}',
|
||||
'notify_version': u'3.7',
|
||||
'address_name': u'Norbert Poilu',
|
||||
'pending_reason': u'multi_currency',
|
||||
'item_number': u'test_ref_2',
|
||||
'item_number': self.reference,
|
||||
'receiver_id': u'dummy',
|
||||
'transaction_subject': u'',
|
||||
'business': u'dummy',
|
||||
@@ -167,46 +116,33 @@ class PaypalForm(PaypalCommon):
|
||||
'shipping': u'0.00',
|
||||
'payer_email': u'tde+buyer@odoo.com',
|
||||
'payment_type': u'instant',
|
||||
'mc_gross': u'1.95',
|
||||
'mc_gross': str(self.amount),
|
||||
'ipn_track_id': u'866df2ccd444b',
|
||||
'quantity': u'1'
|
||||
}
|
||||
|
||||
# should raise error about unknown tx
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['payment.transaction'].form_feedback(paypal_post_data, 'paypal')
|
||||
self.env['payment.transaction']._handle_feedback_data('paypal', paypal_post_data)
|
||||
|
||||
# create tx
|
||||
tx = self.env['payment.transaction'].create({
|
||||
'amount': 1.95,
|
||||
'acquirer_id': self.paypal.id,
|
||||
'currency_id': self.currency_euro.id,
|
||||
'reference': 'test_ref_2',
|
||||
'partner_name': 'Norbert Buyer',
|
||||
'partner_country_id': self.country_france.id})
|
||||
tx = self.create_transaction(flow='redirect')
|
||||
|
||||
# validate it
|
||||
tx.form_feedback(paypal_post_data, 'paypal')
|
||||
# check
|
||||
# Validate the transaction (pending feedback)
|
||||
self.env['payment.transaction']._handle_feedback_data('paypal', paypal_post_data)
|
||||
self.assertEqual(tx.state, 'pending', 'paypal: wrong state after receiving a valid pending notification')
|
||||
self.assertEqual(tx.state_message, 'multi_currency', 'paypal: wrong state message after receiving a valid pending notification')
|
||||
self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification')
|
||||
|
||||
# update tx
|
||||
tx.write({
|
||||
'state': 'draft',
|
||||
'acquirer_reference': False})
|
||||
# Reset the transaction
|
||||
tx.write({'state': 'draft', 'acquirer_reference': False})
|
||||
|
||||
# update notification from paypal
|
||||
# Validate the transaction ('completed' feedback)
|
||||
paypal_post_data['payment_status'] = 'Completed'
|
||||
# validate it
|
||||
tx.form_feedback(paypal_post_data, 'paypal')
|
||||
# check
|
||||
self.env['payment.transaction']._handle_feedback_data('paypal', paypal_post_data)
|
||||
self.assertEqual(tx.state, 'done', 'paypal: wrong state after receiving a valid pending notification')
|
||||
self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification')
|
||||
self.assertEqual(fields.Datetime.to_string(tx.date), '2013-11-18 11:21:19', 'paypal: wrong validation date')
|
||||
|
||||
def test_21_paypal_compute_fees(self):
|
||||
def test_fees_computation(self):
|
||||
#If the merchant needs to keep 100€, the transaction will be equal to 103.30€.
|
||||
#In this way, Paypal will take 103.30 * 2.9% + 0.30 = 3.30€
|
||||
#And the merchant will take 103.30 - 3.30 = 100€
|
||||
@@ -215,5 +151,5 @@ class PaypalForm(PaypalCommon):
|
||||
'fees_int_fixed': 0.30,
|
||||
'fees_int_var': 2.90,
|
||||
})
|
||||
total_fee = self.paypal.paypal_compute_fees(100, False, False)
|
||||
total_fee = self.paypal._compute_fees(100, False, False)
|
||||
self.assertEqual(round(total_fee, 2), 3.3, 'Wrong computation of the Paypal fees')
|
||||
|
||||
@@ -1,40 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<template id="paypal_form">
|
||||
<div>
|
||||
<input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/>
|
||||
<input type="hidden" name="cmd" t-att-value="cmd"/>
|
||||
<input type="hidden" name="business" t-att-value="business"/>
|
||||
<input type="hidden" name="bn" value="OdooInc_SP" />
|
||||
<input type="hidden" name="item_name" t-att-value="item_name"/>
|
||||
<input type="hidden" name="item_number" t-att-value="item_number"/>
|
||||
<input type="hidden" name="amount" t-att-value="amount"/>
|
||||
<input t-if="handling" type="hidden" name="handling"
|
||||
t-att-value="handling"/>
|
||||
<input type="hidden" name="currency_code" t-att-value="currency_code"/>
|
||||
<!-- partner / address data -->
|
||||
<input type="hidden" name="address1" t-att-value="address1"/>
|
||||
<input type="hidden" name="city" t-att-value="city"/>
|
||||
<input type="hidden" name="country" t-att-value="country"/>
|
||||
<input type="hidden" name="email" t-att-value="email"/>
|
||||
<input type="hidden" name="first_name" t-att-value="first_name"/>
|
||||
<input type="hidden" name="last_name" t-att-value="last_name"/>
|
||||
<input type="hidden" name="zip" t-att-value="zip_code"/>
|
||||
<input type="hidden" name="rm" value="2"/>
|
||||
<input t-if='state' type="hidden" name="state"
|
||||
t-att-value='state'/>
|
||||
<!-- after payment parameters -->
|
||||
<input t-if='custom' type="hidden" name="custom"
|
||||
t-att-value='custom'/>
|
||||
<!-- URLs -->
|
||||
<input t-if="paypal_return" type="hidden" name='return'
|
||||
t-att-value="paypal_return"/>
|
||||
<input t-if="acquirer.paypal_use_ipn" type="hidden" name='notify_url'
|
||||
t-att-value="notify_url"/>
|
||||
<input t-if="cancel_return" type="hidden" name="cancel_return"
|
||||
t-att-value="cancel_return"/>
|
||||
</div>
|
||||
</template>
|
||||
</data>
|
||||
|
||||
<!-- https://developer.paypal.com/docs/paypal-payments-standard/integration-guide/formbasics -->
|
||||
<template id="redirect_form">
|
||||
<form t-att-action="api_url" method="post">
|
||||
<input type="hidden" name="address1" t-att-value="address1"/>
|
||||
<input type="hidden" name="amount" t-att-value="amount"/>
|
||||
<input type="hidden" name="business" t-att-value="business"/>
|
||||
<input type="hidden" name="cancel_return" t-att-value="return_url"/>
|
||||
<input type="hidden" name="city" t-att-value="city"/>
|
||||
<input type="hidden" name="cmd" value="_xclick"/>
|
||||
<input type="hidden" name="country" t-att-value="country"/>
|
||||
<input type="hidden" name="currency_code" t-att-value="currency_code"/>
|
||||
<input type="hidden" name="email" t-att-value="email"/>
|
||||
<input type="hidden" name="first_name" t-att-value="first_name"/>
|
||||
<input t-if="handling"
|
||||
type="hidden" name="handling" t-att-value="handling"/>
|
||||
<input type="hidden" name="item_name" t-att-value="item_name"/>
|
||||
<input type="hidden" name="item_number" t-att-value="item_number"/>
|
||||
<input type="hidden" name="last_name" t-att-value="last_name"/>
|
||||
<input type="hidden" name="lc" t-att-value="lc"/>
|
||||
<input t-if="notify_url"
|
||||
type="hidden" name="notify_url" t-att-value="notify_url"/>
|
||||
<input type="hidden" name="return" t-att-value="return_url"/>
|
||||
<input type="hidden" name="rm" value="2"/>
|
||||
<input t-if="state"
|
||||
type="hidden" name="state" t-att-value="state"/>
|
||||
<input type="hidden" name="zip" t-att-value="zip_code"/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="acquirer_form_paypal" model="ir.ui.view">
|
||||
<field name="name">acquirer.form.paypal</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', '!=', 'paypal')]}">
|
||||
<field name="paypal_email_account" attrs="{'required':[ ('provider', '=', 'paypal'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="paypal_seller_account"/>
|
||||
<field name="paypal_pdt_token"/>
|
||||
<field name="paypal_use_ipn" attrs="{'required':[ ('provider', '=', 'paypal'), ('state', '!=', 'disabled')]}"/>
|
||||
<a colspan="2" href="https://www.odoo.com/documentation/user/online/ecommerce/shopper_experience/paypal.html" target="_blank">How to configure your paypal account?</a>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="payment_acquirer_form" model="ir.ui.view">
|
||||
<field name="name">PayPal 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', '!=', 'paypal')]}">
|
||||
<field name="paypal_email_account"
|
||||
attrs="{'required':[('provider', '=', 'paypal'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="paypal_seller_account"/>
|
||||
<!-- This field should no longer be used but is kept in debug mode for the time
|
||||
being, until we are sure that the verification protocol of IPN can be used
|
||||
for DPT notifications -->
|
||||
<field name="paypal_pdt_token" groups="base.group_no_one"/>
|
||||
<field name="paypal_use_ipn"
|
||||
attrs="{'required':[('provider', '=', 'paypal'), ('state', '!=', 'disabled')]}"/>
|
||||
<a href="https://www.odoo.com/documentation/user/general/payment_acquirers/paypal.html"
|
||||
target="_blank"
|
||||
colspan="2">
|
||||
How to configure your paypal account?
|
||||
</a>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="transaction_form_paypal" model="ir.ui.view">
|
||||
<field name="name">acquirer.transaction.form.paypal</field>
|
||||
<field name="model">payment.transaction</field>
|
||||
<field name="inherit_id" ref="payment.transaction_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='acquirer_reference']" position="after">
|
||||
<field name="paypal_txn_type" readonly="1" attrs="{'invisible': [('provider', '!=', 'paypal')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="payment_transaction_form" model="ir.ui.view">
|
||||
<field name="name">PayPal Transaction Form</field>
|
||||
<field name="model">payment.transaction</field>
|
||||
<field name="inherit_id" ref="payment.payment_transaction_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='acquirer_reference']" position="after">
|
||||
<field name="paypal_type"
|
||||
readonly="1"
|
||||
attrs="{'invisible': [('provider', '!=', 'paypal')]}"
|
||||
groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user