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. +

+
    +
  • eCommerce
  • +
+
+ +
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 @@ + + + + + + + +