From 33c094d218b9afc33260cb92ae80273aa8fef463 Mon Sep 17 00:00:00 2001
From: Martin Trigaux
Date: Mon, 4 Apr 2016 11:37:34 +0200
Subject: [PATCH] [ADD] payment_stripe
https://stripe.com/docs/api
Closes #11587
---
addons/payment/data/payment_acquirer.xml | 16 ++
addons/payment_stripe/__init__.py | 5 +
addons/payment_stripe/__openerp__.py | 19 ++
addons/payment_stripe/controllers/__init__.py | 3 +
addons/payment_stripe/controllers/main.py | 51 +++++
.../data/payment_stripe_data.xml | 18 ++
addons/payment_stripe/models/__init__.py | 5 +
.../payment_stripe/models/payment_acquirer.py | 68 +++++++
.../payment_stripe/models/payment_method.py | 50 +++++
.../models/payment_transaction.py | 101 ++++++++++
.../static/description/icon.png | Bin 0 -> 6322 bytes
.../static/src/img/stripe_icon.png | Bin 0 -> 3237 bytes
addons/payment_stripe/static/src/js/stripe.js | 39 ++++
addons/payment_stripe/tests/__init__.py | 2 +
addons/payment_stripe/tests/test_stripe.py | 174 ++++++++++++++++++
.../payment_stripe/views/payment_acquirer.xml | 16 ++
.../views/payment_stripe_template.xml | 8 +
addons/payment_stripe/views/stripe.xml | 66 +++++++
18 files changed, 641 insertions(+)
create mode 100644 addons/payment_stripe/__init__.py
create mode 100644 addons/payment_stripe/__openerp__.py
create mode 100644 addons/payment_stripe/controllers/__init__.py
create mode 100644 addons/payment_stripe/controllers/main.py
create mode 100644 addons/payment_stripe/data/payment_stripe_data.xml
create mode 100644 addons/payment_stripe/models/__init__.py
create mode 100644 addons/payment_stripe/models/payment_acquirer.py
create mode 100644 addons/payment_stripe/models/payment_method.py
create mode 100644 addons/payment_stripe/models/payment_transaction.py
create mode 100644 addons/payment_stripe/static/description/icon.png
create mode 100644 addons/payment_stripe/static/src/img/stripe_icon.png
create mode 100644 addons/payment_stripe/static/src/js/stripe.js
create mode 100644 addons/payment_stripe/tests/__init__.py
create mode 100644 addons/payment_stripe/tests/test_stripe.py
create mode 100644 addons/payment_stripe/views/payment_acquirer.xml
create mode 100644 addons/payment_stripe/views/payment_stripe_template.xml
create mode 100644 addons/payment_stripe/views/stripe.xml
diff --git a/addons/payment/data/payment_acquirer.xml b/addons/payment/data/payment_acquirer.xml
index bc95fbc2b5a..e7fec99c1ca 100644
--- a/addons/payment/data/payment_acquirer.xml
+++ b/addons/payment/data/payment_acquirer.xml
@@ -112,4 +112,20 @@
+
+
+ Stripe
+
+
+
+
+
+ A payment gateway to accept online payments via credit cards.
+
+
+
+
+
diff --git a/addons/payment_stripe/__init__.py b/addons/payment_stripe/__init__.py
new file mode 100644
index 00000000000..5efbd253ebb
--- /dev/null
+++ b/addons/payment_stripe/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import models
+import controllers
diff --git a/addons/payment_stripe/__openerp__.py b/addons/payment_stripe/__openerp__.py
new file mode 100644
index 00000000000..80e5f5395c7
--- /dev/null
+++ b/addons/payment_stripe/__openerp__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+{
+ 'name': 'Stripe Payment Acquirer',
+ 'category': 'Hidden',
+ 'summary': 'Payment Acquirer: Stripe Implementation',
+ 'version': '1.0',
+ 'description': """Stripe Payment Acquirer""",
+ 'depends': ['payment'],
+ 'data': [
+ 'views/stripe.xml',
+ 'views/payment_acquirer.xml',
+ 'views/payment_stripe_template.xml',
+ 'data/payment_stripe_data.xml',
+ ],
+ 'images': ['static/description/icon.png'],
+ 'installable': True,
+ 'license': 'OEEL-1',
+}
diff --git a/addons/payment_stripe/controllers/__init__.py b/addons/payment_stripe/controllers/__init__.py
new file mode 100644
index 00000000000..bbd183e955b
--- /dev/null
+++ b/addons/payment_stripe/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import main
diff --git a/addons/payment_stripe/controllers/main.py b/addons/payment_stripe/controllers/main.py
new file mode 100644
index 00000000000..3075ec68399
--- /dev/null
+++ b/addons/payment_stripe/controllers/main.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+import logging
+import pprint
+import werkzeug
+
+from odoo import http
+from odoo.http import request
+
+_logger = logging.getLogger(__name__)
+
+
+class StripeController(http.Controller):
+
+ @http.route(['/payment/stripe/s2s/create_json'], type='json', auth='public')
+ def stripe_s2s_create_json(self, **kwargs):
+ acquirer_id = int(kwargs.get('acquirer_id'))
+ acquirer = request.env['payment.acquirer'].browse(acquirer_id)
+ return acquirer.s2s_process(kwargs)
+
+ @http.route(['/payment/stripe/s2s/create'], type='http', auth='public')
+ def stripe_s2s_create(self, **post):
+ acquirer_id = int(post.get('acquirer_id'))
+ acquirer = request.env['payment.acquirer'].browse(acquirer_id)
+ acquirer.s2s_process(post)
+ return werkzeug.utils.redirect(post.get('return_url', '/'))
+
+ @http.route(['/payment/stripe/create_charge'], type='json', auth='public')
+ def stripe_create_charge(self, **post):
+ """ Create a payment transaction
+
+ Expects the result from the user input from checkout.js popup"""
+ acquirer = request.env['payment.acquirer'].browse(int(post.get('acquirer_id')))
+ order = request.env['sale.order'].sudo().browse(int(request.session.get('sale_order_id')))
+ tx = request.env['payment.transaction'].sudo().create({
+ 'acquirer_id': acquirer.id,
+ 'reference': str(post.get('invoice_num')),
+ 'amount': float(post.get('amount')),
+ 'currency_id': request.env['res.currency'].search([('name', '=', post.get('currency'))], limit=1).id,
+ 'partner_id': request.env.user.partner_id.id,
+ 'sale_order_id': order.id
+ })
+ order.write({
+ 'payment_acquirer_id': acquirer.id,
+ 'payment_tx_id': tx.id
+ })
+ request.session['sale_transaction_id'] = tx.id
+ response = tx._create_stripe_charge(tokenid=post['tokenid'])
+ _logger.info('Stripe: entering form_feedback with post data %s', pprint.pformat(response))
+ if response:
+ request.env['payment.transaction'].sudo().form_feedback(response, 'stripe')
+ return post.pop('return_url', '/')
diff --git a/addons/payment_stripe/data/payment_stripe_data.xml b/addons/payment_stripe/data/payment_stripe_data.xml
new file mode 100644
index 00000000000..49ac0838a35
--- /dev/null
+++ b/addons/payment_stripe/data/payment_stripe_data.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ Stripe
+
+ stripe
+
+
+
+ test
+ You will be prompt with Stripe Payment page for payment information.
]]>
+ dummy
+ dummy
+
+
+
diff --git a/addons/payment_stripe/models/__init__.py b/addons/payment_stripe/models/__init__.py
new file mode 100644
index 00000000000..49cea96f9de
--- /dev/null
+++ b/addons/payment_stripe/models/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+
+import payment_transaction
+import payment_acquirer
+import payment_method
diff --git a/addons/payment_stripe/models/payment_acquirer.py b/addons/payment_stripe/models/payment_acquirer.py
new file mode 100644
index 00000000000..1a0f6f3ed6e
--- /dev/null
+++ b/addons/payment_stripe/models/payment_acquirer.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-'8' "-*-"
+
+from odoo import api, fields, models
+
+# Force the API version to avoid breaking in case of update on Stripe side
+# cf https://stripe.com/docs/api#versioning
+# changelog https://stripe.com/docs/upgrades#api-changelog
+STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'}
+
+
+class PaymentAcquirerStripe(models.Model):
+ _inherit = 'payment.acquirer'
+
+ stripe_secret_key = fields.Char(required_if_provider='stripe')
+ stripe_publishable_key = fields.Char(required_if_provider='stripe')
+
+ @api.model
+ def _get_providers(self):
+ providers = super(PaymentAcquirerStripe, self)._get_providers()
+ providers.append(['stripe', 'Stripe'])
+ return providers
+
+ @api.multi
+ def stripe_form_generate_values(self, tx_values):
+ self.ensure_one()
+ stripe_tx_values = dict(tx_values)
+ temp_stripe_tx_values = {
+ 'amount': tx_values.get('amount'),
+ 'currency': tx_values.get('currency') and tx_values.get('currency').name or '',
+ 'address_line1': tx_values['partner_address'],
+ 'address_city': tx_values['partner_city'],
+ 'address_country': tx_values['partner_country'] and tx_values['partner_country'].name or '',
+ 'email': tx_values['partner_email'],
+ 'address_zip': tx_values['partner_zip'],
+ 'name': tx_values['partner_name'],
+ 'phone': tx_values['partner_phone'],
+ }
+
+ temp_stripe_tx_values['returndata'] = stripe_tx_values.pop('return_url', '')
+ stripe_tx_values.update(temp_stripe_tx_values)
+ return stripe_tx_values
+
+ @api.model
+ def _get_stripe_api_url(self):
+ return 'api.stripe.com/v1'
+
+ @api.model
+ def stripe_s2s_form_process(self, data):
+ payment_method = self.env['payment.method'].sudo().create({
+ 'cc_number': data['cc_number'],
+ 'cc_holder_name': data['cc_holder_name'],
+ 'cc_expiry': data['cc_expiry'],
+ 'cc_brand': data['cc_brand'],
+ 'cvc': data['cvc'],
+ 'acquirer_id': int(data['acquirer_id']),
+ 'partner_id': int(data['partner_id'])
+ })
+ return payment_method.id
+
+ @api.multi
+ def stripe_s2s_form_validate(self, data):
+ self.ensure_one()
+
+ # mandatory fields
+ for field_name in ["cc_number", "cvc", "cc_holder_name", "cc_expiry", "cc_brand"]:
+ if not data.get(field_name):
+ return False
+ return True
diff --git a/addons/payment_stripe/models/payment_method.py b/addons/payment_stripe/models/payment_method.py
new file mode 100644
index 00000000000..8fb07704b2e
--- /dev/null
+++ b/addons/payment_stripe/models/payment_method.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-'8' "-*-"
+
+import requests
+
+from odoo import api, models
+from odoo.exceptions import UserError
+from . import payment_acquirer as pa
+
+
+class PaymentMethodStripe(models.Model):
+ _inherit = 'payment.method'
+
+ @api.model
+ def stripe_create(self, values):
+ res = None
+ payment_acquirer = self.env['payment.acquirer'].browse(values.get('acquirer_id'))
+ url_token = 'https://%s/tokens' % payment_acquirer._get_stripe_api_url()
+ url_customer = 'https://%s/customers' % payment_acquirer._get_stripe_api_url()
+ if values['cc_number']:
+ payment_params = {
+ 'card[number]': values['cc_number'].replace(' ', ''),
+ 'card[exp_month]': str(values['cc_expiry'][:2]),
+ 'card[exp_year]': str(values['cc_expiry'][-2:]),
+ 'card[cvc]': values['cvc'],
+ }
+ r = requests.post(url_token,
+ auth=(payment_acquirer.stripe_secret_key, ''),
+ params=payment_params,
+ headers=pa.STRIPE_HEADERS)
+ token = r.json()
+ if token.get('id'):
+ customer_params = {
+ 'source': token['id']
+ }
+ r = requests.post(url_customer,
+ auth=(payment_acquirer.stripe_secret_key, ''),
+ params=customer_params,
+ headers=pa.STRIPE_HEADERS)
+ customer = r.json()
+ res = {
+ 'acquirer_ref': customer['id'],
+ 'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name'])
+ }
+ elif token.get('error'):
+ raise UserError(token['error']['message'])
+
+ # pop credit card info to info sent to create
+ for field_name in ["cc_number", "cvc", "cc_holder_name", "cc_expiry", "cc_brand"]:
+ values.pop(field_name)
+ return res
diff --git a/addons/payment_stripe/models/payment_transaction.py b/addons/payment_stripe/models/payment_transaction.py
new file mode 100644
index 00000000000..66409f0b942
--- /dev/null
+++ b/addons/payment_stripe/models/payment_transaction.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-'8' "-*-"
+
+import json
+import logging
+import requests
+import urllib
+
+from odoo import api, fields, models, _
+from odoo.addons.payment.models.payment_acquirer import ValidationError
+from odoo.tools.safe_eval import safe_eval
+from . import payment_acquirer as pa
+
+
+_logger = logging.getLogger(__name__)
+
+
+class PaymentTransactionStripe(models.Model):
+ _inherit = 'payment.transaction'
+
+ def _create_stripe_charge(self, acquirer_ref=None, tokenid=None):
+ api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url())
+ charge_params = {
+ 'amount': int(self.amount*100), # Stripe takes amount in cents (https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support)
+ 'currency': self.currency_id.name,
+ 'metadata[reference]': self.reference
+ }
+ if acquirer_ref:
+ charge_params['customer'] = acquirer_ref
+ if tokenid:
+ charge_params['card'] = str(tokenid)
+ r = requests.post(api_url_charge,
+ auth=(self.acquirer_id.stripe_secret_key, ''),
+ params=charge_params,
+ headers=pa.STRIPE_HEADERS)
+ return r.json()
+
+ @api.multi
+ def stripe_s2s_do_transaction(self, **kwargs):
+ self.ensure_one()
+ result = self._create_stripe_charge(acquirer_ref=self.payment_method_id.acquirer_ref)
+ return self._stripe_s2s_validate_tree(result)
+
+ @api.model
+ def _stripe_form_get_tx_from_data(self, data):
+ """ Given a data dict coming from stripe, verify it and find the related
+ transaction record. """
+ reference = data['metadata']['reference']
+ if not reference:
+ error_msg = _('Stripe: invalid reply received from provider, missing reference')
+ _logger.error(error_msg, data['metadata'])
+ raise ValidationError(error_msg)
+ tx = self.search([('reference', '=', reference)])
+ if not tx:
+ error_msg = (_('Stripe: no order found for reference %s') % reference)
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+ elif len(tx) > 1:
+ error_msg = (_('Stripe: %s orders found for reference %s') % (len(tx), reference))
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+ return tx[0]
+
+ @api.multi
+ def _stripe_s2s_validate_tree(self, tree):
+ self.ensure_one()
+ if self.state not in ('draft', 'pending'):
+ _logger.info('Stripe: trying to validate an already validated tx (ref %s)', self.reference)
+ return True
+
+ status = tree.get('status')
+ if status == 'succeeded':
+ self.write({
+ 'state': 'done',
+ 'date_validate': fields.datetime.now(),
+ 'acquirer_reference': tree.get('id'),
+ })
+ if self.callback_eval:
+ safe_eval(self.callback_eval, {'self': self})
+ return True
+ else:
+ error = tree['error']['message']
+ _logger.warn(error)
+ self.sudo().write({
+ 'state': 'error',
+ 'state_message': error,
+ 'acquirer_reference': tree.get('id'),
+ 'date_validate': fields.datetime.now(),
+ })
+ return False
+
+ @api.model
+ def _stripe_form_get_invalid_parameters(self, tx, data):
+ invalid_parameters = []
+ reference = data['metadata']['reference']
+ if reference != tx.reference:
+ invalid_parameters.append(('Reference', reference, tx.reference))
+ return invalid_parameters
+
+ @api.model
+ def _stripe_form_validate(self, tx, data):
+ return tx._stripe_s2s_validate_tree(data)
diff --git a/addons/payment_stripe/static/description/icon.png b/addons/payment_stripe/static/description/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..c16523f0a5c7b25fe7283d6ab1c494b9ba76df82
GIT binary patch
literal 6322
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
literal 0
HcmV?d00001
diff --git a/addons/payment_stripe/static/src/img/stripe_icon.png b/addons/payment_stripe/static/src/img/stripe_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..79e4e3cbab8e0fc58c2ab53e2470eb9a2b2214e6
GIT binary patch
literal 3237
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
NhI%J4={o0p{{uDr(eVHP
literal 0
HcmV?d00001
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
new file mode 100644
index 00000000000..a2399f5ea25
--- /dev/null
+++ b/addons/payment_stripe/static/src/js/stripe.js
@@ -0,0 +1,39 @@
+odoo.define('payment_stripe.stripe', function(require) {
+ "use strict";
+ var ajax = require('web.ajax');
+ var handler = StripeCheckout.configure({
+ key: $("input[name='stripe_key']").val(),
+ locale: 'auto',
+ closed: function() {
+ if (!handler.isTokenGenerate) {
+ $('#pay_stripe')
+ .removeAttr('disabled')
+ .find('i').remove();
+ }
+ },
+ token: function(token, args) {
+ handler.isTokenGenerate = true;
+ ajax.jsonRpc("/payment/stripe/create_charge", 'call', {
+ tokenid: token.id,
+ email: token.email,
+ amount: $("input[name='amount']").val(),
+ acquirer_id: $("#acquirer_stripe").val(),
+ currency: $("input[name='currency']").val(),
+ invoice_num: $("input[name='invoice_num']").val(),
+ return_url: $("input[name='return_url']").val()
+ }).done(function(data){
+ handler.isTokenGenerate = false;
+ window.location.href = data;
+ });
+ },
+ });
+
+ $('#pay_stripe').on('click', function(e) {
+ // Open Checkout with further options
+ if(!$(this).find('i').length)
+ $(this).append('');
+ $(this).attr('disabled','disabled');
+ handler.open();
+ e.preventDefault();
+ });
+});
diff --git a/addons/payment_stripe/tests/__init__.py b/addons/payment_stripe/tests/__init__.py
new file mode 100644
index 00000000000..13c90c428f9
--- /dev/null
+++ b/addons/payment_stripe/tests/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+import test_stripe
diff --git a/addons/payment_stripe/tests/test_stripe.py b/addons/payment_stripe/tests/test_stripe.py
new file mode 100644
index 00000000000..a29b4a84be2
--- /dev/null
+++ b/addons/payment_stripe/tests/test_stripe.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+
+import odoo
+from odoo import fields
+from odoo.addons.payment.tests.common import PaymentAcquirerCommon
+from odoo.tools import mute_logger
+
+
+@odoo.tests.common.at_install(True)
+@odoo.tests.common.post_install(True)
+class StripeCommon(PaymentAcquirerCommon):
+
+ def setUp(self):
+ super(StripeCommon, self).setUp()
+ self.currency_euro = self.env['res.currency'].search([('name', '=', 'EUR')], limit=1)
+ # get the stripe account
+ self.stripe_id = self.env.ref('payment.payment_acquirer_stripe').id
+
+
+@odoo.tests.common.at_install(True)
+@odoo.tests.common.post_install(True)
+class StripeTest(StripeCommon):
+
+ def test_10_stripe_s2s(self):
+ stripe = self.env['payment.acquirer'].browse(self.stripe_id)
+ self.assertEqual(stripe.environment, 'test', 'test without test environment')
+
+ # Add Stripe credentials
+ stripe.write({
+ 'stripe_secret_key': 'sk_test_bldAlqh1U24L5HtRF9mBFpK7',
+ 'stripe_publishable_key': 'pk_test_0TKSyYSZS9AcS4keZ2cxQQCW',
+ })
+
+ # Create payment meethod for Stripe
+ payment_method = self.env['payment.method'].create({
+ 'acquirer_id': self.stripe_id,
+ 'partner_id': self.buyer_id,
+ 'cc_number': '4242424242424242',
+ 'cc_expiry': '02 / 26',
+ 'cc_brand': 'visa',
+ 'cvc': '111',
+ 'cc_holder_name': 'Johndoe',
+ })
+
+ # Create transaction
+ tx = self.env['payment.transaction'].create({
+ 'reference': 'test_ref_%s' % fields.date.today(),
+ 'currency_id': self.currency_euro.id,
+ 'acquirer_id': self.stripe_id,
+ 'partner_id': self.buyer_id,
+ 'payment_method_id': payment_method.id,
+ 'type': 'server2server',
+ 'amount': 115.0
+ })
+ tx.stripe_s2s_do_transaction()
+
+ # Check state
+ self.assertEqual(tx.state, 'done', 'Stripe: Transcation has been discarded.')
+
+ def test_20_stripe_form_render(self):
+ stripe = self.env['payment.acquirer'].browse(self.stripe_id)
+ self.assertEqual(stripe.environment, 'test', 'test without test environment')
+
+ # ----------------------------------------
+ # Test: button direct rendering
+ # ----------------------------------------
+ form_values = {
+ 'amount': 320.0,
+ 'currency': 'EUR',
+ 'address_line1': 'Huge Street 2/543',
+ 'address_city': 'Sin City',
+ 'address_country': 'Belgium',
+ 'email': 'norbert.buyer@example.com',
+ 'address_zip': '1000',
+ 'name': 'Norbert Buyer',
+ 'phone': '0032 12 34 56 78'
+ }
+
+ # render the button
+ res = stripe.render('SO404', 320.0, self.currency_euro.id, values=self.buyer_values)
+ post_url = "https://checkout.stripe.com/checkout.js"
+ email = "norbert.buyer@example.com"
+ # check form result
+ if "https://checkout.stripe.com/checkout.js" in res[0]:
+ self.assertEqual(post_url, 'https://checkout.stripe.com/checkout.js', 'Stripe: wrong form POST url')
+ # Generated and received
+ if email in res[0]:
+ self.assertEqual(
+ email, form_values.get('email'),
+ 'Stripe: wrong value for input %s: received %s instead of %s' % (email, email, form_values.get('email'))
+ )
+
+ def test_30_stripe_form_management(self):
+ stripe = self.env['payment.acquirer'].browse(self.stripe_id)
+ self.assertEqual(stripe.environment, 'test', 'test without test environment')
+
+ # typical data posted by Stripe after client has successfully paid
+ stripe_post_data = {
+ u'amount': 4700,
+ u'amount_refunded': 0,
+ u'application_fee': None,
+ u'balance_transaction': u'txn_172xfnGMfVJxozLwssrsQZyT',
+ u'captured': True,
+ u'created': 1446529775,
+ u'currency': u'eur',
+ u'customer': None,
+ u'description': None,
+ u'destination': None,
+ u'dispute': None,
+ u'failure_code': None,
+ u'failure_message': None,
+ u'fraud_details': {},
+ u'id': u'ch_172xfnGMfVJxozLwEjSfpfxD',
+ u'invoice': None,
+ u'livemode': False,
+ u'metadata': {u'reference': u'SO100'},
+ u'object': u'charge',
+ u'paid': True,
+ u'receipt_email': None,
+ u'receipt_number': None,
+ u'refunded': False,
+ u'refunds': {u'data': [],
+ u'has_more': False,
+ u'object': u'list',
+ u'total_count': 0,
+ u'url': u'/v1/charges/ch_172xfnGMfVJxozLwEjSfpfxD/refunds'},
+ u'shipping': None,
+ u'source': {u'address_city': None,
+ u'address_country': None,
+ u'address_line1': None,
+ u'address_line1_check': None,
+ u'address_line2': None,
+ u'address_state': None,
+ u'address_zip': None,
+ u'address_zip_check': None,
+ u'brand': u'Visa',
+ u'country': u'US',
+ u'customer': None,
+ u'cvc_check': u'pass',
+ u'dynamic_last4': None,
+ u'exp_month': 2,
+ u'exp_year': 2022,
+ u'fingerprint': u'9tJA9bUEuvEb3Ell',
+ u'funding': u'credit',
+ u'id': u'card_172xfjGMfVJxozLw1QO6gYNM',
+ u'last4': u'4242',
+ u'metadata': {},
+ u'name': u'norbert.buyer@example.com',
+ u'object': u'card',
+ u'tokenization_method': None},
+ u'statement_descriptor': None,
+ u'status': u'succeeded'}
+
+ tx = self.env['payment.transaction'].create({
+ 'amount': 4700,
+ 'acquirer_id': self.stripe_id,
+ 'currency_id': self.currency_euro.id,
+ 'reference': 'SO100',
+ 'partner_name': 'Norbert Buyer',
+ 'partner_country_id': self.country_france_id})
+
+ # validate it
+ tx.form_feedback(stripe_post_data, 'stripe')
+ self.assertEqual(tx.state, 'done', 'Stripe: validation did not put tx into done state')
+ self.assertEqual(tx.acquirer_reference, stripe_post_data.get('id'), 'Stripe: validation did not update tx id')
+ # reset tx
+ tx.write({'state': 'draft', 'date_validate': False, 'acquirer_reference': False})
+ # simulate an error
+ stripe_post_data['status'] = 'error'
+ stripe_post_data.update({u'error': {u'message': u"Your card's expiration year is invalid.", u'code': u'invalid_expiry_year', u'type': u'card_error', u'param': u'exp_year'}})
+ with mute_logger('openerp.addons.payment_stripe.models.payment_transaction'):
+ tx.form_feedback(stripe_post_data, 'stripe')
+ # check state
+ self.assertEqual(tx.state, 'error', 'Stipe: erroneous validation did not put tx into error state')
diff --git a/addons/payment_stripe/views/payment_acquirer.xml b/addons/payment_stripe/views/payment_acquirer.xml
new file mode 100644
index 00000000000..db3c2be5c45
--- /dev/null
+++ b/addons/payment_stripe/views/payment_acquirer.xml
@@ -0,0 +1,16 @@
+
+
+
+ payment.acquirer.form.inherit
+ payment.acquirer
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/payment_stripe/views/payment_stripe_template.xml b/addons/payment_stripe/views/payment_stripe_template.xml
new file mode 100644
index 00000000000..74a7d7e03e2
--- /dev/null
+++ b/addons/payment_stripe/views/payment_stripe_template.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/addons/payment_stripe/views/stripe.xml b/addons/payment_stripe/views/stripe.xml
new file mode 100644
index 00000000000..ea089287b22
--- /dev/null
+++ b/addons/payment_stripe/views/stripe.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+