From 30da37fbb801009f59dd9c0b34f8b3b3aac3e3e3 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Wed, 23 May 2018 16:40:41 +0000 Subject: [PATCH] Move Stripe to ERPNext --- ...rpnext_integrations.py => integrations.py} | 5 + .../doctype/payment_plan/__init__.py | 0 .../doctype/payment_plan/payment_plan.js | 8 + .../doctype/payment_plan/payment_plan.json | 195 +++++++++++ .../doctype/payment_plan/payment_plan.py | 10 + .../doctype/payment_plan/test_payment_plan.js | 23 ++ .../doctype/payment_plan/test_payment_plan.py | 10 + .../doctype/stripe_settings/__init__.py | 0 .../stripe_settings/stripe_settings.js | 8 + .../stripe_settings/stripe_settings.json | 315 ++++++++++++++++++ .../stripe_settings/stripe_settings.py | 163 +++++++++ .../stripe_settings/test_stripe_settings.js | 23 ++ .../stripe_settings/test_stripe_settings.py | 10 + .../includes/integrations/stripe_checkout.js | 85 +++++ .../pages/integrations/stripe_checkout.css | 113 +++++++ .../pages/integrations/stripe_checkout.html | 56 ++++ .../pages/integrations/stripe_checkout.py | 64 ++++ requirements.txt | 1 + 18 files changed, 1089 insertions(+) rename erpnext/config/{erpnext_integrations.py => integrations.py} (84%) create mode 100644 erpnext/erpnext_integrations/doctype/payment_plan/__init__.py create mode 100644 erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.js create mode 100644 erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.json create mode 100644 erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.py create mode 100644 erpnext/erpnext_integrations/doctype/payment_plan/test_payment_plan.js create mode 100644 erpnext/erpnext_integrations/doctype/payment_plan/test_payment_plan.py create mode 100644 erpnext/erpnext_integrations/doctype/stripe_settings/__init__.py create mode 100644 erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.js create mode 100644 erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.json create mode 100644 erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.py create mode 100644 erpnext/erpnext_integrations/doctype/stripe_settings/test_stripe_settings.js create mode 100644 erpnext/erpnext_integrations/doctype/stripe_settings/test_stripe_settings.py create mode 100644 erpnext/templates/includes/integrations/stripe_checkout.js create mode 100644 erpnext/templates/pages/integrations/stripe_checkout.css create mode 100644 erpnext/templates/pages/integrations/stripe_checkout.html create mode 100644 erpnext/templates/pages/integrations/stripe_checkout.py diff --git a/erpnext/config/erpnext_integrations.py b/erpnext/config/integrations.py similarity index 84% rename from erpnext/config/erpnext_integrations.py rename to erpnext/config/integrations.py index e27b7cd04f..14d917f704 100644 --- a/erpnext/config/erpnext_integrations.py +++ b/erpnext/config/integrations.py @@ -7,6 +7,11 @@ def get_data(): "label": _("Payments"), "icon": "fa fa-star", "items": [ + { + "type": "doctype", + "name": "Stripe Settings", + "description": _("Stripe payment gateway settings"), + }, { "type": "doctype", "name": "GoCardless Settings", diff --git a/erpnext/erpnext_integrations/doctype/payment_plan/__init__.py b/erpnext/erpnext_integrations/doctype/payment_plan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.js b/erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.js new file mode 100644 index 0000000000..50a41ecc2d --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.js @@ -0,0 +1,8 @@ +// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Payment Plan', { + refresh: function(frm) { + + } +}); diff --git a/erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.json b/erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.json new file mode 100644 index 0000000000..7550cb4135 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.json @@ -0,0 +1,195 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:payment_plan_id", + "beta": 0, + "creation": "2018-05-23 10:17:31.108746", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "payment_plan", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "payment_plan_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "ID", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "payment_gateway", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Payment Gateway", + "length": 0, + "no_copy": 0, + "options": "Payment Gateway Account", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Monthly", + "description": "", + "fieldname": "recurrence", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Recurrence", + "length": 0, + "no_copy": 0, + "options": "Daily\nWeekly\nMonthly\nEvery 3 Months\nEvery 6 Months\nYearly", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2018-05-23 18:25:48.200621", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "Payment Plan", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "payment_plan", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.py b/erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.py new file mode 100644 index 0000000000..d61adec880 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/payment_plan/payment_plan.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class PaymentPlan(Document): + pass diff --git a/erpnext/erpnext_integrations/doctype/payment_plan/test_payment_plan.js b/erpnext/erpnext_integrations/doctype/payment_plan/test_payment_plan.js new file mode 100644 index 0000000000..330e71c2ab --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/payment_plan/test_payment_plan.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Payment Plan", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Payment Plan + () => frappe.tests.make('Payment Plan', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/erpnext_integrations/doctype/payment_plan/test_payment_plan.py b/erpnext/erpnext_integrations/doctype/payment_plan/test_payment_plan.py new file mode 100644 index 0000000000..5b97868bdf --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/payment_plan/test_payment_plan.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestPaymentPlan(unittest.TestCase): + pass diff --git a/erpnext/erpnext_integrations/doctype/stripe_settings/__init__.py b/erpnext/erpnext_integrations/doctype/stripe_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.js b/erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.js new file mode 100644 index 0000000000..e5cddd3bf3 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Stripe Settings', { + refresh: function(frm) { + + } +}); diff --git a/erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.json b/erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.json new file mode 100644 index 0000000000..4129e74d74 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.json @@ -0,0 +1,315 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:gateway_name", + "beta": 0, + "creation": "2017-03-09 17:18:29.458397", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "gateway_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Payment Gateway Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "publishable_key", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Publishable Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "secret_key", + "fieldtype": "Password", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Secret Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "header_img", + "fieldtype": "Attach Image", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Header Image", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_7", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "redirect_url", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Redirect URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2018-05-23 18:15:55.584782", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "Stripe Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.py b/erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.py new file mode 100644 index 0000000000..227f13897c --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/stripe_settings/stripe_settings.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe import _ +from six.moves.urllib.parse import urlencode +from frappe.utils import get_url, call_hook_method, cint, flt +from frappe.integrations.utils import make_get_request, make_post_request, create_request_log, create_payment_gateway +import stripe + +class StripeSettings(Document): + supported_currencies = [ + "AED", "ALL", "ANG", "ARS", "AUD", "AWG", "BBD", "BDT", "BIF", "BMD", "BND", + "BOB", "BRL", "BSD", "BWP", "BZD", "CAD", "CHF", "CLP", "CNY", "COP", "CRC", "CVE", "CZK", "DJF", + "DKK", "DOP", "DZD", "EGP", "ETB", "EUR", "FJD", "FKP", "GBP", "GIP", "GMD", "GNF", "GTQ", "GYD", + "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS", "INR", "ISK", "JMD", "JPY", "KES", "KHR", "KMF", + "KRW", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "MAD", "MDL", "MNT", "MOP", "MRO", "MUR", "MVR", + "MWK", "MXN", "MYR", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "PAB", "PEN", "PGK", "PHP", "PKR", + "PLN", "PYG", "QAR", "RUB", "SAR", "SBD", "SCR", "SEK", "SGD", "SHP", "SLL", "SOS", "STD", "SVC", + "SZL", "THB", "TOP", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VND", "VUV", "WST", + "XAF", "XOF", "XPF", "YER", "ZAR" + ] + + currency_wise_minimum_charge_amount = { + 'JPY': 50, 'MXN': 10, 'DKK': 2.50, 'HKD': 4.00, 'NOK': 3.00, 'SEK': 3.00, + 'USD': 0.50, 'AUD': 0.50, 'BRL': 0.50, 'CAD': 0.50, 'CHF': 0.50, 'EUR': 0.50, + 'GBP': 0.30, 'NZD': 0.50, 'SGD': 0.50 + } + + def on_update(self): + create_payment_gateway('Stripe-' + self.gateway_name, settings='Stripe Settings', controller=self.gateway_name) + call_hook_method('payment_gateway_enabled', gateway='Stripe-' + self.gateway_name) + if not self.flags.ignore_mandatory: + self.validate_stripe_credentails() + + def validate_stripe_credentails(self): + if self.publishable_key and self.secret_key: + header = {"Authorization": "Bearer {0}".format(self.get_password(fieldname="secret_key", raise_exception=False))} + try: + make_get_request(url="https://api.stripe.com/v1/charges", headers=header) + except Exception: + frappe.throw(_("Seems Publishable Key or Secret Key is wrong !!!")) + + def validate_transaction_currency(self, currency): + if currency not in self.supported_currencies: + frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency)) + + def validate_minimum_transaction_amount(self, currency, amount): + if currency in self.currency_wise_minimum_charge_amount: + if flt(amount) < self.currency_wise_minimum_charge_amount.get(currency, 0.0): + frappe.throw(_("For currency {0}, the minimum transaction amount should be {1}").format(currency, + self.currency_wise_minimum_charge_amount.get(currency, 0.0))) + + def get_payment_url(self, **kwargs): + return get_url("./integrations/stripe_checkout?{0}".format(urlencode(kwargs))) + + def create_request(self, data): + self.data = frappe._dict(data) + stripe.api_key = self.get_password(fieldname="secret_key", raise_exception=False) + stripe.default_http_client = stripe.http_client.RequestsClient() + + try: + self.integration_request = create_request_log(self.data, "Host", "Stripe") + if frappe.db.get_value("Payment Request", self.data.reference_docname, 'is_a_subscription'): + self.payment_plan = frappe.db.get_value("Payment Request", self.data.reference_docname, 'payment_plan') + return self.create_subscription_on_stripe() + else: + return self.create_charge_on_stripe() + + except Exception: + frappe.log_error(frappe.get_traceback()) + return{ + "redirect_to": frappe.redirect_to_message(_('Server Error'), _("It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account.")), + "status": 401 + } + + def create_charge_on_stripe(self): + try: + charge = stripe.Charge.create(amount=cint(flt(self.data.amount)*100), currency=self.data.currency, source=self.data.stripe_token_id, description=self.data.description) + + if charge.captured == True: + self.integration_request.db_set('status', 'Completed', update_modified=False) + self.flags.status_changed_to = "Completed" + + else: + frappe.log_error(str(resp), 'Stripe Payment not completed') + + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass + + return self.finalize_request() + + def create_subscription_on_stripe(self): + items = [ + { + "plan": self.payment_plan + } + ] + + try: + customer = stripe.Customer.create(description=self.data.payer_name, email=self.data.payer_email, source=self.data.stripe_token_id) + subscription = stripe.Subscription.create(customer=customer, items=items) + + if subscription.status == "active": + self.integration_request.db_set('status', 'Completed', update_modified=False) + self.flags.status_changed_to = "Completed" + + else: + self.integration_request.db_set('status', 'Failed', update_modified=False) + frappe.log_error(str(resp), 'Stripe Payment not completed') + + except: + self.integration_request.db_set('status', 'Failed', update_modified=False) + frappe.log_error(frappe.get_traceback()) + # failed + pass + + return self.finalize_request() + + def finalize_request(self): + redirect_to = self.data.get('redirect_to') or None + redirect_message = self.data.get('redirect_message') or None + status = self.integration_request.status + + if self.flags.status_changed_to == "Completed": + if self.data.reference_doctype and self.data.reference_docname: + custom_redirect_to = None + try: + custom_redirect_to = frappe.get_doc(self.data.reference_doctype, + self.data.reference_docname).run_method("on_payment_authorized", self.flags.status_changed_to) + except Exception: + frappe.log_error(frappe.get_traceback()) + + if custom_redirect_to: + redirect_to = custom_redirect_to + + redirect_url = 'payment-success' + + if self.redirect_url: + redirect_url = self.redirect_url + redirect_to = None + else: + redirect_url = 'payment-failed' + + if redirect_to: + redirect_url += '?' + urlencode({'redirect_to': redirect_to}) + if redirect_message: + redirect_url += '&' + urlencode({'redirect_message': redirect_message}) + + return { + "redirect_to": redirect_url, + "status": status + } + +def get_gateway_controller(doc): + payment_request = frappe.get_doc("Payment Request", doc) + gateway_controller = frappe.db.get_value("Payment Gateway", payment_request.payment_gateway, "gateway_controller") + return gateway_controller diff --git a/erpnext/erpnext_integrations/doctype/stripe_settings/test_stripe_settings.js b/erpnext/erpnext_integrations/doctype/stripe_settings/test_stripe_settings.js new file mode 100644 index 0000000000..b491ba5737 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/stripe_settings/test_stripe_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Stripe Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Stripe Settings + () => frappe.tests.make('Stripe Settings', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/erpnext_integrations/doctype/stripe_settings/test_stripe_settings.py b/erpnext/erpnext_integrations/doctype/stripe_settings/test_stripe_settings.py new file mode 100644 index 0000000000..3403a6279e --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/stripe_settings/test_stripe_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestStripeSettings(unittest.TestCase): + pass diff --git a/erpnext/templates/includes/integrations/stripe_checkout.js b/erpnext/templates/includes/integrations/stripe_checkout.js new file mode 100644 index 0000000000..1ce3b12b7a --- /dev/null +++ b/erpnext/templates/includes/integrations/stripe_checkout.js @@ -0,0 +1,85 @@ +var stripe = Stripe("{{ publishable_key }}"); + +var elements = stripe.elements(); + +var style = { + base: { + color: '#32325d', + lineHeight: '18px', + fontFamily: '"Helvetica Neue", Helvetica, sans-serif', + fontSmoothing: 'antialiased', + fontSize: '16px', + '::placeholder': { + color: '#aab7c4' + } + }, + invalid: { + color: '#fa755a', + iconColor: '#fa755a' + } +}; + +var card = elements.create('card', { + hidePostalCode: true, + style: style +}); + +card.mount('#card-element'); + +function setOutcome(result) { + + if (result.token) { + $('#submit').prop('disabled', true) + $('#submit').html(__('Processing...')) + frappe.call({ + method:"erpnext.templates.pages.integrations.stripe_checkout.make_payment", + freeze:true, + headers: {"X-Requested-With": "XMLHttpRequest"}, + args: { + "stripe_token_id": result.token.id, + "data": JSON.stringify({{ frappe.form_dict|json }}), + "reference_doctype": "{{ reference_doctype }}", + "reference_docname": "{{ reference_docname }}" + }, + callback: function(r) { + if (r.message.status == "Completed") { + $('#submit').hide() + $('.success').show() + setTimeout(function() { + window.location.href = r.message.redirect_to + }, 2000); + } else { + $('#submit').hide() + $('.error').show() + setTimeout(function() { + window.location.href = r.message.redirect_to + }, 2000); + } + } + }); + + } else if (result.error) { + $('.error').html() = result.error.message; + $('.error').show() + } +} + +card.on('change', function(event) { + var displayError = document.getElementById('card-errors'); + if (event.error) { + displayError.textContent = event.error.message; + } else { + displayError.textContent = ''; + } +}); + +frappe.ready(function() { + $('#submit').off("click").on("click", function(e) { + e.preventDefault(); + var extraDetails = { + name: $('input[name=cardholder-name]').val(), + email: $('input[name=cardholder-email]').val() + } + stripe.createToken(card, extraDetails).then(setOutcome); + }) +}); diff --git a/erpnext/templates/pages/integrations/stripe_checkout.css b/erpnext/templates/pages/integrations/stripe_checkout.css new file mode 100644 index 0000000000..04efc79530 --- /dev/null +++ b/erpnext/templates/pages/integrations/stripe_checkout.css @@ -0,0 +1,113 @@ +.StripeElement { + background-color: white; + height: 40px; + padding: 10px 12px; + border-radius: 4px; + border: 1px solid transparent; + box-shadow: 0 1px 3px 0 #e6ebf1; + -webkit-transition: box-shadow 150ms ease; + transition: box-shadow 150ms ease; +} + +.StripeElement--focus { + box-shadow: 0 1px 3px 0 #cfd7df; +} + +.StripeElement--invalid { + border-color: #fa755a; +} + +.StripeElement--webkit-autofill { + background-color: #fefde5 !important; +} + +.stripe #payment-form { + margin-top: 80px; +} + +.stripe button { + float: right; + display: block; + background: #5e64ff; + color: white; + box-shadow: 0 7px 14px 0 rgba(49, 49, 93, 0.10), 0 3px 6px 0 rgba(0, 0, 0, 0.08); + border-radius: 4px; + border: 0; + margin-top: 20px; + font-size: 15px; + font-weight: 400; + max-width: 40%; + height: 40px; + line-height: 38px; + outline: none; +} + +.stripe button:hover, .stripe button:focus { + background: #2b33ff; + border-color: #0711ff; +} + +.stripe button:active { + background: #5e64ff; +} + +.stripe button:disabled { + background: #515e80; +} + +.stripe .group { + background: white; + box-shadow: 2px 7px 14px 2px rgba(49, 49, 93, 0.10), 0 3px 6px 0 rgba(0, 0, 0, 0.08); + border-radius: 4px; + margin-bottom: 20px; +} + +.stripe label { + position: relative; + color: #8898AA; + font-weight: 300; + height: 40px; + line-height: 40px; + margin-left: 20px; + display: block; +} + +.stripe .group label:not(:last-child) { + border-bottom: 1px solid #F0F5FA; +} + +.stripe label>span { + width: 20%; + text-align: right; + float: left; +} + +.current-card { + margin-left: 20px; +} + +.field { + background: transparent; + font-weight: 300; + border: 0; + color: #31325F; + outline: none; + padding-right: 10px; + padding-left: 10px; + cursor: text; + width: 70%; + height: 40px; + float: right; +} + +.field::-webkit-input-placeholder { + color: #CFD7E0; +} + +.field::-moz-placeholder { + color: #CFD7E0; +} + +.field:-ms-input-placeholder { + color: #CFD7E0; +} diff --git a/erpnext/templates/pages/integrations/stripe_checkout.html b/erpnext/templates/pages/integrations/stripe_checkout.html new file mode 100644 index 0000000000..b52428667b --- /dev/null +++ b/erpnext/templates/pages/integrations/stripe_checkout.html @@ -0,0 +1,56 @@ +{% extends "templates/web.html" %} + +{% block title %} Payment {% endblock %} + +{%- block header -%} +{% endblock %} + +{% block script %} + + +{% endblock %} + +{%- block page_content -%} + +
+
+ +

{{description}}

+
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+ +
+ + +
+
+
+
+ + +{% endblock %} diff --git a/erpnext/templates/pages/integrations/stripe_checkout.py b/erpnext/templates/pages/integrations/stripe_checkout.py new file mode 100644 index 0000000000..e9c1ff3fab --- /dev/null +++ b/erpnext/templates/pages/integrations/stripe_checkout.py @@ -0,0 +1,64 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt, cint, fmt_money +import json +from erpnext.erpnext_integrations.doctype.stripe_settings.stripe_settings import get_gateway_controller + +no_cache = 1 +no_sitemap = 1 + +expected_keys = ('amount', 'title', 'description', 'reference_doctype', 'reference_docname', + 'payer_name', 'payer_email', 'order_id', 'currency') + +def get_context(context): + context.no_cache = 1 + + # all these keys exist in form_dict + if not (set(expected_keys) - set(list(frappe.form_dict))): + for key in expected_keys: + context[key] = frappe.form_dict[key] + + gateway_controller = get_gateway_controller(context.reference_docname) + context.publishable_key = get_api_key(context.reference_docname, gateway_controller) + context.image = get_header_image(context.reference_docname, gateway_controller) + + context['amount'] = fmt_money(amount=context['amount'], currency=context['currency']) + + if frappe.db.get_value(context.reference_doctype, context.reference_docname, "is_a_subscription"): + payment_plan = frappe.db.get_value(context.reference_doctype, context.reference_docname, "payment_plan") + recurrence = frappe.db.get_value("Payment Plan", payment_plan, "recurrence") + + context['amount'] = context['amount'] + " " + _(recurrence) + + else: + frappe.redirect_to_message(_('Some information is missing'), + _('Looks like someone sent you to an incomplete URL. Please ask them to look into it.')) + frappe.local.flags.redirect_location = frappe.local.response.location + raise frappe.Redirect + +def get_api_key(doc, gateway_controller): + publishable_key = frappe.db.get_value("Stripe Settings", gateway_controller, "publishable_key") + if cint(frappe.form_dict.get("use_sandbox")): + publishable_key = frappe.conf.sandbox_publishable_key + + return publishable_key + +def get_header_image(doc, gateway_controller): + header_image = frappe.db.get_value("Stripe Settings", gateway_controller, "header_img") + return header_image + +@frappe.whitelist(allow_guest=True) +def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None): + data = json.loads(data) + + data.update({ + "stripe_token_id": stripe_token_id + }) + + gateway_controller = get_gateway_controller(reference_docname) + data = frappe.get_doc("Stripe Settings", gateway_controller).create_request(data) + frappe.db.commit() + return data diff --git a/requirements.txt b/requirements.txt index 13055ac2ec..2da865c277 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ python-stdnum braintree gocardless_pro woocommerce +stripe