diff --git a/addons/iap/__init__.py b/addons/iap/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..dc5e6b693d19dcacd224b7ab27b26f75e66cb7b2 --- /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 0000000000000000000000000000000000000000..35124df76ebcb421b3161f3349523852c5b7e43c --- /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 0000000000000000000000000000000000000000..4af61c83a0d780e9dadbc5a223d3307328c0d9c3 --- /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 0000000000000000000000000000000000000000..87394fd053b12ade58d5090ae3d0dda3f16a32dd --- /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 0000000000000000000000000000000000000000..0eeb34c40cd59cebcd2af7d9a4ad3be4f47768b2 --- /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 0000000000000000000000000000000000000000..2935ce2b7461322df1443129814d4091aed33234 --- /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 0000000000000000000000000000000000000000..3c8d330013eadbc04768d3df4227e568a48d9122 --- /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 0000000000000000000000000000000000000000..b3490da2eda2d26ad3fabb3f22725cad9ce898b9 --- /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 0000000000000000000000000000000000000000..37cddd72aa8ca60c4fd678cf4365c42ba510fa24 --- /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 0000000000000000000000000000000000000000..9b52a4ec445b5cc7c008c653ce6a1a37938550f1 --- /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 0000000000000000000000000000000000000000..0e7294ca5867da870a3f116749aebe34d251e834 --- /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 0000000000000000000000000000000000000000..ebd64fedee8f5fb84c992658e45b6b6402b20c8c --- /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> diff --git a/addons/web_settings_dashboard/static/src/js/dashboard.js b/addons/web_settings_dashboard/static/src/js/dashboard.js index 060f7dca61275faca7be21f98497ce7ea9cc0e56..7848b4934fd1b4c6b0d5c1da1df14204c1245fed 100644 --- a/addons/web_settings_dashboard/static/src/js/dashboard.js +++ b/addons/web_settings_dashboard/static/src/js/dashboard.js @@ -345,6 +345,7 @@ core.action_registry.add('web_settings_dashboard.main', Dashboard); return { Dashboard: Dashboard, + DashboardApps: DashboardApps, DashboardInvitations: DashboardInvitations, DashboardPlanner: DashboardPlanner, DashboardShare: DashboardShare, diff --git a/doc/_extensions/odoo_ext/static/layout.less b/doc/_extensions/odoo_ext/static/layout.less index c350defa2a1b52faef0d078e1187c94d78ce164a..4c8a5c643869c9e8640ca49f36ef88dbc2fc1653 100644 --- a/doc/_extensions/odoo_ext/static/layout.less +++ b/doc/_extensions/odoo_ext/static/layout.less @@ -103,7 +103,7 @@ main.has_code_col{ > *{ max-width: 100%; } - section { + > section { position: relative; display:block; float: left; @@ -116,7 +116,7 @@ main.has_code_col{ &:before { .code-col(); } - section { + > section { > * { width: 54.633333%; max-width: 600px; diff --git a/doc/_extensions/odoo_ext/static/style.css b/doc/_extensions/odoo_ext/static/style.css index ead78f9101dd2f625e541f22d2e5d4304a050bf1..1442d4bdd6756fb2b4b7a4f770653610c2809599 100644 --- a/doc/_extensions/odoo_ext/static/style.css +++ b/doc/_extensions/odoo_ext/static/style.css @@ -9708,7 +9708,7 @@ main.has_code_col article.doc-body { main.has_code_col article.doc-body > * { max-width: 100%; } -main.has_code_col article.doc-body section { +main.has_code_col article.doc-body > section { position: relative; display: block; float: left; @@ -9730,36 +9730,36 @@ main.has_code_col article.doc-body section { top: 0; right: 0; } - main.has_code_col article.doc-body section > * { + main.has_code_col article.doc-body > section > * { width: 54.633333%; max-width: 600px; float: left; clear: left; } - main.has_code_col article.doc-body section > h1, - main.has_code_col article.doc-body section > h2, - main.has_code_col article.doc-body section > h3, - main.has_code_col article.doc-body section > h4, - main.has_code_col article.doc-body section > h5, - main.has_code_col article.doc-body section > h6 { + main.has_code_col article.doc-body > section > h1, + main.has_code_col article.doc-body > section > h2, + main.has_code_col article.doc-body > section > h3, + main.has_code_col article.doc-body > section > h4, + main.has_code_col article.doc-body > section > h5, + main.has_code_col article.doc-body > section > h6 { width: 100%; float: none; clear: none; } - main.has_code_col article.doc-body section .doc-aside { + main.has_code_col article.doc-body > section .doc-aside { width: 41%; float: none; clear: none; margin-right: 15px; margin-left: 57%; } - main.has_code_col article.doc-body section .doc-aside .content-switcher { + main.has_code_col article.doc-body > section .doc-aside .content-switcher { margin-top: 0; } - main.has_code_col article.doc-body section .doc-aside .content-switcher > ul { + main.has_code_col article.doc-body > section .doc-aside .content-switcher > ul { margin-bottom: 0; } - main.has_code_col article.doc-body section .doc-aside .content-switcher > ul > li { + main.has_code_col article.doc-body > section .doc-aside .content-switcher > ul > li { color: #dcddde; } } diff --git a/doc/_extensions/odoo_ext/translator.py b/doc/_extensions/odoo_ext/translator.py index 443458f33c7755e94e7437b0687c6f3e09ab854a..c2349bca315b919d9b0b57d58c09771d43c90081 100644 --- a/doc/_extensions/odoo_ext/translator.py +++ b/doc/_extensions/odoo_ext/translator.py @@ -142,6 +142,11 @@ class BootstrapTranslator(nodes.NodeVisitor, object): if not self.section_level: self.body.append(u'</section>') + def visit_topic(self, node): + self.body.append(self.starttag(node, 'nav')) + def depart_topic(self, node): + self.body.append(u'</nav>') + def is_compact_paragraph(self, node): parent = node.parent if isinstance(parent, (nodes.document, nodes.compound, diff --git a/doc/index.rst b/doc/index.rst index 21231b7ce3fadedc00eb62fd13eb478ba8852def..8035da789f9ea03364c90d4e0bef14bd7c1a48d9 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,7 +14,7 @@ Index :maxdepth: 2 tutorials - api_integration + webservices setup reference diff --git a/doc/reference.rst b/doc/reference.rst index d83ef3bb4768bdd6107e1506fecddfa6c60d93b2..1b41f4cdbd24a0eb85bc27f7ddc36ec53d43b862 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -21,5 +21,4 @@ Reference reference/reports reference/mixins reference/guidelines - reference/upgrade_api reference/mobile diff --git a/doc/webservices.rst b/doc/webservices.rst new file mode 100644 index 0000000000000000000000000000000000000000..3dad86448ba797ec7b4fb1bd12873284fe01f17b --- /dev/null +++ b/doc/webservices.rst @@ -0,0 +1,13 @@ +:banner: banners/web_service_api.jpg +:types: api + +============ +Web Services +============ + +.. toctree:: + :titlesonly: + + webservices/odoo + webservices/iap + webservices/upgrade diff --git a/doc/webservices/flow.png b/doc/webservices/flow.png new file mode 100644 index 0000000000000000000000000000000000000000..df4bf7cfbbbc24ecda679650eb5c13a467c93659 Binary files /dev/null and b/doc/webservices/flow.png differ diff --git a/doc/webservices/iap.rst b/doc/webservices/iap.rst new file mode 100644 index 0000000000000000000000000000000000000000..18af76900fb8bc09ebe1396cedda1f96b38a1493 --- /dev/null +++ b/doc/webservices/iap.rst @@ -0,0 +1,235 @@ +:types: api + + +:code-column: + +.. _webservices/iap: + +================ +In-App Purchases +================ + +IAP allow providers of ongoing services through Odoo apps to be compensated +for ongoing service use rather than — and possibly instead of — a sole initial +purchase. + +In that context, Odoo acts mostly as a *broker* between the service user +(client) and the service provider (Odoo App developer): + +* users purchase service tokens from Odoo +* service providers draw tokens from the user's Odoo account + +.. attention:: + + This document is intended for *service providers* and presents the latter, + which can be done either via direct JSON-RPC2_ or if you are using Odoo + using the convenience helpers it provides. + +.. image:: flow.png + :align: center + +.. contents:: + :local: + +JSON-RPC2_ Transaction API +========================== + +* The IAP transaction API does not require using Odoo when implementing your + server gateway, calls are standard JSON-RPC2_. +* Calls use different *endpoints* but the same *method* on all endpoints + (``call``). +* Exceptions are returned as JSON-RPC2_ errors, the formal exception name is + available on ``data.name`` for programmatic manipulation. + +.. class:: ServiceKey + + Identifier generated for the provider's service. Each key (and service) + matches a token of a fixed value, as generated by the service provide. + + Multiple types of tokens correspond to multiple services e.g. SMS and MMS + could either be the same service (with an MMS being "worth" multiple SMS) + or could be separate services at separate price points. + + .. danger:: your service key *is a secret*, leaking your service key + allows other application developers to draw credits bought for + your service(s) + +.. class:: UserToken + + Identifier for a user account. + +.. class:: TransactionToken + + Transaction identifier, returned by the authorization process and consumed + by either capturing or cancelling the transaction + +.. exception:: odoo.addons.iap.models.iap.NoCreditError + + Raised during transaction authorization if the credits requested are not + currently available on the account (either not enough credits or too many + pending transactions/existing holds). + +.. exception:: odoo.addons.iap.models.iap.BadAuthError + + Raised by any operation to which a service token is required, if the + service token is invalid. + +Authorize +--------- + +.. function:: /iap/1/authorize + + Verifies that the user's account has at least as ``credit`` available + *and creates a hold (pending transaction) on that amount*. + + Any amount currently on hold by a pending transaction is considered + unavailable to further authorize calls. + + Returns a :class:`TransactionToken` identifying the pending transaction + which can be used to capture (confirm) or cancel said transaction. + + :param ServiceKey key: + :param UserToken account_token: + :param int credit: + :param str description: optional, helps users identify the reason for + charges on their accounts. + :returns: :class:`TransactionToken` if the authorization succeeded. + :raises: :class:`~odoo.addons.iap.models.iap.BadAuthError` if the service token is invalid + :raises: :class:`~odoo.addons.iap.models.iap.NoCreditError` if the account does + :raises: ``TypeError`` if the ``credit`` value is not an integer + +.. rst-class:: doc-aside + +.. code-block:: python + + r = requests.post(ODOO + '/iap/1/authorize', json={ + 'jsonrpc': '2.0', + 'id': None, + 'method': 'call', + 'params': { + 'account_token': user_account, + 'key': SERVICE_KEY, + 'credit': 25, + 'description': "Why this is being charged", + } + }).json() + if 'error' in r: + # handle authorize error + tx = r['result'] + + # provide your service here + +Capture +------- + +.. function:: /iap/1/capture + + Confirms the specified transaction, transferring the reserved credits from + the user's account to the service provider's. + + Capture calls are idempotent: performing capture calls on an already + captured transaction has no further effect. + + :param TransactionToken token: + :param ServiceKey key: + :raises: :class:`~odoo.addons.iap.models.iap.BadAuthError` + +.. rst-class:: doc-aside + +.. code-block:: python + + r2 = requests.post(ODOO + '/iap/1/capture', json={ + 'jsonrpc': '2.0', + 'id': None, + 'method': 'call', + 'params': { + 'token': tx, + 'key': SERVICE_KEY, + } + }).json() + if 'error' in r: + # handle capture error + # otherwise transaction is captured + +Cancel +------ + +.. function:: /iap/1/cancel + + Cancels the specified transaction, releasing the hold on the user's + credits. + + Cancel calls are idempotent: performing capture calls on an already + cancelled transaction has no further effect. + + :param TransactionToken token: + :param ServiceKey key: + :raises: :class:`~odoo.addons.iap.models.iap.BadAuthError` + +.. rst-class:: doc-aside + +.. code-block:: python + + r2 = requests.post(ODOO + '/iap/1/cancel', json={ + 'jsonrpc': '2.0', + 'id': None, + 'method': 'call', + 'params': { + 'token': tx, + 'key': SERVICE_KEY, + } + }).json() + if 'error' in r: + # handle cancel error + # otherwise transaction is cancelled + +Odoo Helpers +============ + +For convenience, if you are implementing your service using Odoo the ``iap`` +module provides a few helpers to make IAP flow even simpler: + +Charging +-------- + +.. class:: odoo.addons.iap.models.iap.charge(env, key, account_token, credit[, description]) + + A *context manager* for authorizing and automatically capturing or + cancelling transactions for use in the backend/proxy. + + Works much like e.g. a cursor context manager: + + * immediately authorizes a transaction with the specified parameters + * executes the ``with`` body + * if the body executes in full without error, captures the transaction + * otherwise cancels it + + :param odoo.api.Environment env: used to retrieve the ``iap.endpoint`` + configuration key + :param ServiceKey key: + :param UserToken token: + :param int credit: + :param str description: + +.. rst-class:: doc-aside + +.. code-block:: python + + @route('/deathstar/superlaser', type='json') + def superlaser(self, user_account, + coordinates, target, + factor=1.0): + """ + :param factor: superlaser power factor, + 0.0 is none, 1.0 is full power + """ + credits = int(MAXIMUM_POWER * factor) + with charge(request.env, SERVICE_KEY, user_account, credits): + # TODO: allow other targets + self.env['systems.planets'].search([ + ('grid', '=', 'M-10'), + ('name', '=', 'Alderaan'), + ]).unlink() + + +.. _JSON-RPC2: http://www.jsonrpc.org/specification diff --git a/doc/api_integration.rst b/doc/webservices/odoo.rst similarity index 99% rename from doc/api_integration.rst rename to doc/webservices/odoo.rst index 612e91ed85422cd43cf952266c2c9abea9c3627e..f68862626a758d5a59f51378d51451bf897798f3 100644 --- a/doc/api_integration.rst +++ b/doc/webservices/odoo.rst @@ -4,9 +4,9 @@ :code-column: -=============== -Web Service API -=============== +=========================== +Odoo Remote Procedure Calls +=========================== Odoo is usually extended internally via modules, but many of its features and all of its data are also available from the outside for external analysis or diff --git a/doc/reference/upgrade_api.rst b/doc/webservices/upgrade.rst similarity index 99% rename from doc/reference/upgrade_api.rst rename to doc/webservices/upgrade.rst index 04edf919e83baddbd4359863c1734c6f651a6cf1..fa168935b2385f51a6671657ff2cc81b355de325 100644 --- a/doc/reference/upgrade_api.rst +++ b/doc/webservices/upgrade.rst @@ -6,9 +6,9 @@ .. _reference/upgrade-api: -=========== -Upgrade API -=========== +================ +Database Upgrade +================ Introduction ~~~~~~~~~~~~