summaryrefslogtreecommitdiff
path: root/addons/iap
diff options
context:
space:
mode:
authorAntony Lesuisse <al@openerp.com>2017-10-04 20:05:05 +0200
committerAntony Lesuisse <al@openerp.com>2017-10-04 20:22:22 +0200
commit6b22476e20fb7eade959194bfabd94d353e2d5ff (patch)
treefbad12338bed7d4f4b38b017cd746878f897b81c /addons/iap
parentea51b6b06f33fa97ace9c55bb86fceaeb60bb18a (diff)
[ADD] iap: In app purchase
IAP allow app publishers to charge for ongoing services. In that context, Odoo acts mostly as a payment platform between the service user (client) and the service provider (Odoo App developer).
Diffstat (limited to 'addons/iap')
-rw-r--r--addons/iap/__init__.py4
-rw-r--r--addons/iap/__manifest__.py23
-rw-r--r--addons/iap/models/__init__.py4
-rw-r--r--addons/iap/models/iap.py157
-rw-r--r--addons/iap/security/ir.model.access.csv3
-rw-r--r--addons/iap/security/ir_rule.xml13
-rw-r--r--addons/iap/static/src/js/crash_manager.js49
-rw-r--r--addons/iap/static/src/js/dashboard.js29
-rw-r--r--addons/iap/static/src/js/iap_credit.js28
-rw-r--r--addons/iap/static/src/xml/iap_templates.xml26
-rw-r--r--addons/iap/views/assets.xml10
-rw-r--r--addons/iap/views/iap_views.xml53
12 files changed, 399 insertions, 0 deletions
diff --git a/addons/iap/__init__.py b/addons/iap/__init__.py
new file mode 100644
index 00000000000..dc5e6b693d1
--- /dev/null
+++ b/addons/iap/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
diff --git a/addons/iap/__manifest__.py b/addons/iap/__manifest__.py
new file mode 100644
index 00000000000..35124df76eb
--- /dev/null
+++ b/addons/iap/__manifest__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+
+{
+ 'name': 'In-App Purchases',
+ 'category': 'Tools',
+ 'summary': 'Basic models and helpers to support In-App purchases.',
+ 'description': """
+This module provides standard tools (account model, context manager and helpers) to support In-App purchases inside Odoo.
+""",
+ 'depends': ['web'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'security/ir_rule.xml',
+ 'views/assets.xml',
+ 'views/iap_views.xml',
+ ],
+ 'qweb': [
+ 'static/src/xml/iap_templates.xml',
+ ],
+ 'auto_install': True,
+}
diff --git a/addons/iap/models/__init__.py b/addons/iap/models/__init__.py
new file mode 100644
index 00000000000..4af61c83a0d
--- /dev/null
+++ b/addons/iap/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import iap
diff --git a/addons/iap/models/iap.py b/addons/iap/models/iap.py
new file mode 100644
index 00000000000..87394fd053b
--- /dev/null
+++ b/addons/iap/models/iap.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+import contextlib
+import logging
+import json
+import uuid
+
+import werkzeug.urls
+import requests
+from requests.packages import urllib3
+
+from odoo import api, fields, models, exceptions
+
+_logger = logging.getLogger(__name__)
+
+DEFAULT_ENDPOINT = 'https://iap.odoo.com'
+
+
+#----------------------------------------------------------
+# Helpers for both clients and proxy
+#----------------------------------------------------------
+def get_endpoint(env):
+ url = env['ir.config_parameter'].sudo().get_param('iap.endpoint', DEFAULT_ENDPOINT)
+ return url
+
+
+#----------------------------------------------------------
+# Helpers for clients
+#----------------------------------------------------------
+class InsufficientCreditError(Exception):
+ pass
+
+
+class AuthenticationError(Exception):
+ pass
+
+
+def jsonrpc(url, method='call', params=None):
+ """
+ Calls the provided JSON-RPC endpoint, unwraps the result and
+ returns JSON-RPC errors as exceptions.
+ """
+ payload = {
+ 'jsonrpc': '2.0',
+ 'method': method,
+ 'params': params,
+ 'id': uuid.uuid4().hex,
+ }
+
+
+ _logger.info('iap jsonrpc %s', url)
+ try:
+ req = requests.post(url, json=payload)
+ response = req.json()
+ if 'error' in response:
+ name = response['error']['data'].get('name').rpartition('.')[-1]
+ message = response['error']['data'].get('message')
+ if name == 'InsufficientCreditError':
+ e_class = InsufficientCreditError
+ elif name == 'AccessError':
+ e_class = exceptions.AccessError
+ else:
+ e_class = exceptions.UserError
+ e = e_class(message)
+ e.data = response['error']['data']
+ raise e
+ return response.get('result')
+ except (ValueError, requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, urllib3.exceptions.MaxRetryError) as e:
+ raise exceptions.AccessError('The url that this service requested returned an error. Please contact the author the app. The url it tried to contact was ' + url)
+
+#----------------------------------------------------------
+# Helpers for proxy
+#----------------------------------------------------------
+@contextlib.contextmanager
+def charge(env, key, account_token, credit, description=None, credit_template=None):
+ """
+ Account charge context manager: takes a hold for ``credit``
+ amount before executing the body, then captures it if there
+ is no error, or cancels it if the body generates an exception.
+
+ :param str key: service identifier
+ :param str account_token: user identifier
+ :param int credit: cost of the body's operation
+ :param str description:
+ """
+ end_point = get_endpoint(env)
+ params = {
+ 'account_token': account_token,
+ 'credit': credit,
+ 'key': key,
+ 'description': description,
+ }
+ try:
+ transaction_token = jsonrpc(endpoint + '/iap/1/authorize', params=params)
+ except InsufficientCreditError as e:
+ if credit_template:
+ arguments = json.loads(e.args[0])
+ arguments['body'] = env['ir.qweb'].render(credit_template)
+ e.args = (json.dumps(arguments),)
+
+ try:
+ yield
+ except Exception as e:
+ params = {
+ 'token': transaction_token,
+ 'key': key,
+ }
+ r = jsonrpc(end_point + '/iap/1/cancel', params=params)
+ raise e
+ else:
+ params = {
+ 'token': transaction_token,
+ 'key': key,
+ }
+ r = jsonrpc(end_point + '/iap/1/capture', params=params) # noqa
+
+
+#----------------------------------------------------------
+# Models for client
+#----------------------------------------------------------
+class IapAccount(models.Model):
+ _name = 'iap.account'
+ _rec_name = 'service_name'
+
+ service_name = fields.Char()
+ account_token = fields.Char(default=lambda s: uuid.uuid4().hex)
+ company_id = fields.Many2one('res.company', default=lambda self: self.env.user.company_id)
+
+ @api.model
+ def get(self, service_name):
+ account = self.search([('service_name', '=', service_name), ('company_id', 'in', [self.env.user.company_id.id, False])])
+ if not account:
+ account = self.create({'service_name': service_name})
+ # Since the account did not exist yet, we will encounter a NoCreditError,
+ # which is going to rollback the database and undo the account creation,
+ # preventing the process to continue any further.
+ self.env.cr.commit()
+ return account
+
+ @api.model
+ def get_credits_url(self, base_url, service_name, credit):
+ dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
+ account_token = self.get(service_name).account_token
+ d = {
+ 'dbuuid': dbuuid,
+ 'service_name': service_name,
+ 'account_token': account_token,
+ 'credit': credit,
+ }
+ return '%s?%s' % (base_url, werkzeug.urls.url_encode(d))
+
+ @api.model
+ def get_account_url(self):
+ route = '/iap/services'
+ endpoint = get_endpoint(self.env)
+ d = {'dbuuid': self.env['ir.config_parameter'].sudo().get_param('database.uuid')}
+
+ return '%s?%s' % (endpoint + route, werkzeug.urls.url_encode(d))
diff --git a/addons/iap/security/ir.model.access.csv b/addons/iap/security/ir.model.access.csv
new file mode 100644
index 00000000000..0eeb34c40cd
--- /dev/null
+++ b/addons/iap/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_client_iap_account_manager,iap.account.manager,model_iap_account,base.group_system,1,1,1,0
+access_client_iap_account_user,iap.account.user,model_iap_account,base.group_user,1,0,0,0 \ No newline at end of file
diff --git a/addons/iap/security/ir_rule.xml b/addons/iap/security/ir_rule.xml
new file mode 100644
index 00000000000..2935ce2b746
--- /dev/null
+++ b/addons/iap/security/ir_rule.xml
@@ -0,0 +1,13 @@
+<odoo>
+ <data>
+ <record id="user_iap_account" model="ir.rule">
+ <field name="name">User IAP Account</field>
+ <field name="model_id" ref="model_iap_account"/>
+ <field name="groups" eval="[(4, ref('base.group_user'))]"/>
+ <!-- partners can CUD services linked to themselves -->
+ <field name="domain_force">[
+ ('company_id', 'in', [user.company_id.id, False]),
+ ]</field>
+ </record>
+ </data>
+</odoo> \ No newline at end of file
diff --git a/addons/iap/static/src/js/crash_manager.js b/addons/iap/static/src/js/crash_manager.js
new file mode 100644
index 00000000000..3c8d330013e
--- /dev/null
+++ b/addons/iap/static/src/js/crash_manager.js
@@ -0,0 +1,49 @@
+odoo.define('iap.CrashManager', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var core = require('web.core');
+var CrashManager = require('web.CrashManager');
+var Dialog = require('web.Dialog');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+CrashManager.include({
+ /**
+ * @override
+ */
+ rpc_error: function (error) {
+ if (error.data.name === "odoo.addons.iap.models.iap.InsufficientCreditError") {
+ var error_data = JSON.parse(error.data.message);
+ ajax.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'iap.account',
+ method: 'get_credits_url',
+ args: [],
+ kwargs: {
+ base_url: error_data.base_url,
+ service_name: error_data.service_name,
+ credit: error_data.credit,
+ }
+ }).then(function (url) {
+ new Dialog(this, {
+ size: 'large',
+ title: error_data.title || _t("Insufficient Balance"),
+ $content: $(QWeb.render('iap.redirect_to_odoo_credit', {
+ data: error_data,
+ })).css('padding', 0),
+ buttons: [
+ {text: 'Buy credits at Odoo', classes : "btn-primary", click: function() {
+ window.open(url, '_blank');
+ }, close:true},
+ {text: _t("Cancel"), close: true}
+ ],
+ }).open();
+ });
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+});
+
+});
diff --git a/addons/iap/static/src/js/dashboard.js b/addons/iap/static/src/js/dashboard.js
new file mode 100644
index 00000000000..b3490da2eda
--- /dev/null
+++ b/addons/iap/static/src/js/dashboard.js
@@ -0,0 +1,29 @@
+odoo.define('iap.Dashboard', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var core = require('web.core');
+var Dashboard = require('web_settings_dashboard');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+Dashboard.Dashboard.include({
+ /**
+ * @override
+ */
+ load_apps: function (data) {
+ var _super = this._super.bind(this);
+ return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'iap.account',
+ method: 'get_account_url',
+ args: [],
+ kwargs: {},
+ }).then(function (url) {
+ data.apps.url = url;
+ return _super(data);
+ });
+ },
+});
+
+});
diff --git a/addons/iap/static/src/js/iap_credit.js b/addons/iap/static/src/js/iap_credit.js
new file mode 100644
index 00000000000..37cddd72aa8
--- /dev/null
+++ b/addons/iap/static/src/js/iap_credit.js
@@ -0,0 +1,28 @@
+odoo.define('iap.redirect_odoo_credit_widget', function(require) {
+"use strict";
+
+var core = require('web.core');
+var framework = require('web.framework');
+var Widget = require('web.Widget');
+var QWeb = core.qweb;
+
+
+var IapOdooCreditRedirect = Widget.extend({
+ template: 'iap.redirect_to_odoo_credit',
+ events : {
+ "click .redirect_confirm" : "odoo_redirect",
+ },
+ init: function (parent, action) {
+ this._super(parent, action);
+ this.url = action.params.url;
+ },
+
+ odoo_redirect: function () {
+ window.open(this.url, '_blank');
+ this.do_action({type: 'ir.actions.act_window_close'});
+ // framework.redirect(this.url);
+ },
+
+});
+core.action_registry.add('iap_odoo_credit_redirect', IapOdooCreditRedirect);
+});
diff --git a/addons/iap/static/src/xml/iap_templates.xml b/addons/iap/static/src/xml/iap_templates.xml
new file mode 100644
index 00000000000..9b52a4ec445
--- /dev/null
+++ b/addons/iap/static/src/xml/iap_templates.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<template id="template" xml:space="preserve">
+
+ <!-- LAYOUT TEMPLATES -->
+ <div t-name="iap.redirect_to_odoo_credit">
+ <t t-if="data.body">
+ <div t-raw="data.body"/>
+ </t>
+ <t t-if="!data.body">
+ <t t-if="data.message">
+ <span t-esc="data.message"/>
+ </t>
+ <t t-if="!data.message">
+ <span>Insufficient credit to perform this service.</span>
+ </t>
+ </t>
+ </div>
+
+ <t t-extend="DashboardApps">
+ <t t-jquery=".o_web_settings_dashboard_pills" t-operation="append">
+ <a class="pull-right" t-att-href="widget.data.url" target="_blank">
+ <i class="fa fa-money fa-2x text-muted"/> In-App Purchases</a>
+ </t>
+ </t>
+
+</template>
diff --git a/addons/iap/views/assets.xml b/addons/iap/views/assets.xml
new file mode 100644
index 00000000000..0e7294ca586
--- /dev/null
+++ b/addons/iap/views/assets.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="assets_backend" name="iap assets" inherit_id="web.assets_backend">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/iap/static/src/js/iap_credit.js"></script>
+ <script type="text/javascript" src="/iap/static/src/js/crash_manager.js"></script>
+ <script type="text/javascript" src="/iap/static/src/js/dashboard.js"></script>
+ </xpath>
+ </template>
+</odoo>
diff --git a/addons/iap/views/iap_views.xml b/addons/iap/views/iap_views.xml
new file mode 100644
index 00000000000..ebd64fedee8
--- /dev/null
+++ b/addons/iap/views/iap_views.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <!-- iap Client Account Views -->
+ <record id="iap_account_view_form" model="ir.ui.view">
+ <field name="name">iap.account.form</field>
+ <field name="model">iap.account</field>
+ <field name="arch" type="xml">
+ <form string="IAP Account">
+ <sheet>
+ <group name="account" string="Account Information">
+ <field name="service_name"/>
+ <field name="company_id"/>
+ <field name="account_token"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+ <record id="iap_account_view_tree" model="ir.ui.view">
+ <field name="name">iap.account.tree</field>
+ <field name="model">iap.account</field>
+ <field name="arch" type="xml">
+ <tree string="IAP Accounts">
+ <field name="service_name"/>
+ <field name="company_id"/>
+ <field name="account_token" readonly="1"/>
+ </tree>
+ </field>
+ </record>
+ <!-- Actions -->
+ <record id="iap_account_action" model="ir.actions.act_window">
+ <field name="name">IAP Account</field>
+ <field name="res_model">iap.account</field>
+ <field name='view_type'>form</field>
+ <field name='view_mode'>tree,form</field>
+ </record>
+
+ <!-- Menus -->
+ <menuitem
+ id="iap_root_menu"
+ name="IAP"
+ parent="base.menu_custom"
+ sequence="4"/>
+
+ <menuitem
+ id="iap_account_menu"
+ name="IAP Accounts"
+ parent="iap_root_menu"
+ action="iap_account_action"
+ sequence="10"/>
+
+</odoo>