summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Trigaux <mat@odoo.com>2016-04-04 11:37:34 +0200
committerMartin Trigaux <mat@odoo.com>2016-04-04 17:09:08 +0200
commit33c094d218b9afc33260cb92ae80273aa8fef463 (patch)
tree7629a6bbb536b3fed29006f86049ea76a7da51b9
parent8b596e8e1b33962011635c85d12bcc088f45732c (diff)
[ADD] payment_stripe
https://stripe.com/docs/api Closes #11587
-rw-r--r--addons/payment/data/payment_acquirer.xml16
-rw-r--r--addons/payment_stripe/__init__.py5
-rw-r--r--addons/payment_stripe/__openerp__.py19
-rw-r--r--addons/payment_stripe/controllers/__init__.py3
-rw-r--r--addons/payment_stripe/controllers/main.py51
-rw-r--r--addons/payment_stripe/data/payment_stripe_data.xml18
-rw-r--r--addons/payment_stripe/models/__init__.py5
-rw-r--r--addons/payment_stripe/models/payment_acquirer.py68
-rw-r--r--addons/payment_stripe/models/payment_method.py50
-rw-r--r--addons/payment_stripe/models/payment_transaction.py101
-rw-r--r--addons/payment_stripe/static/description/icon.pngbin0 -> 6322 bytes
-rw-r--r--addons/payment_stripe/static/src/img/stripe_icon.pngbin0 -> 3237 bytes
-rw-r--r--addons/payment_stripe/static/src/js/stripe.js39
-rw-r--r--addons/payment_stripe/tests/__init__.py2
-rw-r--r--addons/payment_stripe/tests/test_stripe.py174
-rw-r--r--addons/payment_stripe/views/payment_acquirer.xml16
-rw-r--r--addons/payment_stripe/views/payment_stripe_template.xml8
-rw-r--r--addons/payment_stripe/views/stripe.xml66
18 files changed, 641 insertions, 0 deletions
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 @@
</ul>
</field>
</record>
+
+ <record id="payment_acquirer_stripe" model="payment.acquirer">
+ <field name="name">Stripe</field>
+ <field name="view_template_id" ref="default_acquirer_button"/>
+ <field name="image" type="base64" file="payment_stripe/static/description/icon.png"/>
+ <field name="module_id" ref="base.module_payment_stripe"/>
+ <field name="description" type="html">
+ <p>
+ A payment gateway to accept online payments via credit cards.
+ </p>
+ <ul>
+ <li><i class="fa fa-check"/>eCommerce</li>
+ </ul>
+ </field>
+
+ </record>
</odoo>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data noupdate="1">
+ <record id="payment.payment_acquirer_stripe" model="payment.acquirer">
+ <field name="name">Stripe</field>
+ <field name="image" type="base64" file="payment_stripe/static/description/icon.png"/>
+ <field name="provider">stripe</field>
+ <field name="company_id" ref="base.main_company"/>
+ <field name="view_template_id" ref="stripe_acquirer_button"/>
+ <field name="registration_view_template_id" ref="stripe_s2s_form"/>
+ <field name="environment">test</field>
+ <field name="pre_msg"><![CDATA[
+<p>You will be prompt with Stripe Payment page for payment information.</p>]]></field>
+ <field name="stripe_secret_key">dummy</field>
+ <field name="stripe_publishable_key">dummy</field>
+ </record>
+ </data>
+</odoo>
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 00000000000..c16523f0a5c
--- /dev/null
+++ b/addons/payment_stripe/static/description/icon.png
Binary files differ
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 00000000000..79e4e3cbab8
--- /dev/null
+++ b/addons/payment_stripe/static/src/img/stripe_icon.png
Binary files differ
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('<i class="fa fa-spinner fa-spin"/>');
+ $(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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="acquirer_form_stripe" model="ir.ui.view">
+ <field name="name">payment.acquirer.form.inherit</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='after'>
+ <group attrs="{'invisible': [('provider', '!=', 'stripe')]}">
+ <field name="stripe_secret_key" password="True"/>
+ <field name="stripe_publishable_key" password="True"/>
+ </group>
+ </xpath>
+ </field>
+ </record>
+</odoo>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="payment_stripe_redirect" name="Payment Stripe">
+ <script type="text/javascript">
+ window.location.href = '<t t-esc="return_url"/>';
+ </script>
+ </template>
+</odoo>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <template id="stripe_acquirer_button">
+ <form t-if="acquirer" method="post" target="_self">
+ <script src="https://checkout.stripe.com/checkout.js"></script>
+ <input type='hidden' name='amount' t-att-value="amount"/>
+ <input type="hidden" name='return_url' t-att-value='returndata'/>
+ <!--Order Information -->
+ <input type='hidden' name='invoice_num' t-att-value='reference'/>
+ <input type='hidden' name='currency' t-att-value='currency'/>
+ <!-- Billing Information-->
+ <input type='hidden' name='name' t-att-value='name'/>
+ <input type='hidden' name='address_line1' t-att-value='address_line1'/>
+ <input type='hidden' name='address_city' t-att-value='address_city'/>
+ <input type='hidden' name='address_zip' t-att-value='address_zip'/>
+ <input type='hidden' name='address_country' t-att-value='address_country'/>
+ <input type='hidden' name='phone' t-att-value='phone'/>
+ <input type='hidden' name='email' t-att-value='email'/>
+ <input id="acquirer_stripe" type='hidden' name="acquirer" t-att-value="acquirer.id"/>
+ <input type='hidden' name="stripe_key" t-att-value="acquirer.stripe_publishable_key"/>
+ <!-- Submit-->
+ <button id="pay_stripe" width="100px" t-att-class="submit_class">
+ <img t-if="not submit_txt" src="/payment_stripe/static/src/img/stripe_icon.png"/>
+ <span t-if="submit_txt"><t t-esc="submit_txt"/> <span class="fa fa-long-arrow-right"/></span>
+ </button>
+ <script type="text/javascript" src="/payment_stripe/static/src/js/stripe.js"></script>
+ </form>
+ </template>
+
+ <template id="stripe_s2s_form">
+ <form method="post" t-att-action="'/payment/stripe/s2s/create' if not json else '/payment/stripe/s2s/create_json'">
+ <div t-att-class="'row' if bootstrap_formatting else ''">
+ <div t-att-class="'form-group col-md-8' if bootstrap_formatting else 'form-group'">
+ <label class="control-label" for="cc_number">Card Number</label>
+ <input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;"/>
+ <div class="card_placeholder"></div>
+ <div class="visa"></div>
+ <input type="hidden" name="cc_brand" value=""/>
+ </div>
+ <div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
+ <label class="control-label" for="cvc">CVC</label>
+ <input type="text" name="cvc" id="cvc" class="form-control" maxlength="4" placeholder="CVC"/>
+ </div>
+ <div t-att-class="'form-group col-md-7' if bootstrap_formatting else 'form-group'">
+ <label class="control-label" for="cc_holder_name">Card Holder Name</label>
+ <input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control"/>
+ </div>
+ <div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
+ <label class="control-label" for="cc_expires_mm">Expiration Date</label>
+ <input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="MM / YY"/>
+ </div>
+ <input type="hidden" name="acquirer_id" t-att-value="id"/>
+ <input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
+ <input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
+ <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
+ <div class="clearfix"/>
+ </div>
+ <button t-att-class="submit_class" class="btn btn-primary" type="submit">
+ <span t-if="not submit_txt">Confirm <span class="fa fa-long-arrow-right"></span></span>
+ <span t-if="submit_txt"><t t-esc="submit_txt"/></span>
+ </button>
+ </form>
+ </template>
+ </data>
+</odoo>