diff options
| author | Antoine Prieels <anp@odoo.com> | 2020-03-25 14:09:24 +0000 |
|---|---|---|
| committer | Pierre Masereel <pim@odoo.com> | 2020-09-29 11:44:46 +0000 |
| commit | e6dc4ea2e6c321829ed2fe8c1c7b0e58db354935 (patch) | |
| tree | f18015990f7d66ff0db3d1954b881982a2fa4015 /addons/pos_adyen | |
| parent | 761639b56b0c9210c77e277f01cea45725b82ef6 (diff) | |
[ADD] adyen_platforms,pos_adyen: Add support for Adyen for Platforms
The goal is to add a nice payment solution to our users.
- Why Adyen?
- Worldwide
- Manages local payment solutions
- Easy onboarding
- For both PoS and eCommerce
- Why Adyen for Platforms?
- The onboarding will be done directly from the customer's DB.
- It's quite complicated for SMEs to get a partnership with Adyen as
they have high requirements for transaction volume. By registering
Odoo as a Platform with Adyen Marketpay, we allow our users to
register as sub-merchants under a common account, removing this
constraint.
As all users are grouped under a common account, all requests need to
provide the same API key. As this key cannot be shared, all requests to
the Adyen API will need to be proxied. A new proxy will be created on
`paymentproxy.odoo.com`.
To initiate a payment, 3 values are required:
- `account_holder_code`: This code is provided by Adyen and is used to
identify the sub-merchant under the common account.
- `adyen_uuid`: This ID is used internally to identify the user.
- `proxy_token`: This token is generated by the proxy during the
account creation and is used to sign all requests.
The DB of the customer is the only host who has all the information
required to modify the sub-merchant info or initiate a payment. If
this information leak, the `proxy_token` can be reset on the proxy to
revoke all access to the API.
Before accessing the form to create an account, users will be
redirected to www.odoo.com where they will be asked to log in. This
ensures that we have contact information for this sub-merchant.
TaskID: 2129218
Diffstat (limited to 'addons/pos_adyen')
| -rw-r--r-- | addons/pos_adyen/__manifest__.py | 6 | ||||
| -rw-r--r-- | addons/pos_adyen/models/__init__.py | 2 | ||||
| -rw-r--r-- | addons/pos_adyen/models/adyen_account.py | 107 | ||||
| -rw-r--r-- | addons/pos_adyen/models/pos_payment_method.py | 71 | ||||
| -rw-r--r-- | addons/pos_adyen/models/res_config_settings.py | 13 | ||||
| -rw-r--r-- | addons/pos_adyen/security/ir.model.access.csv | 4 | ||||
| -rw-r--r-- | addons/pos_adyen/static/src/js/models.js | 1 | ||||
| -rw-r--r-- | addons/pos_adyen/static/src/js/payment_adyen.js | 11 | ||||
| -rw-r--r-- | addons/pos_adyen/views/adyen_account_views.xml | 68 | ||||
| -rw-r--r-- | addons/pos_adyen/views/pos_payment_method_views.xml | 25 | ||||
| -rw-r--r-- | addons/pos_adyen/views/res_config_settings_views.xml | 20 |
11 files changed, 288 insertions, 40 deletions
diff --git a/addons/pos_adyen/__manifest__.py b/addons/pos_adyen/__manifest__.py index b291bdaabc2..d34a34d5b47 100644 --- a/addons/pos_adyen/__manifest__.py +++ b/addons/pos_adyen/__manifest__.py @@ -8,12 +8,14 @@ 'summary': 'Integrate your POS with an Adyen payment terminal', 'description': '', 'data': [ + 'security/ir.model.access.csv', + 'views/adyen_account_views.xml', 'views/pos_config_views.xml', 'views/pos_payment_method_views.xml', 'views/point_of_sale_assets.xml', + 'views/res_config_settings_views.xml', ], - 'depends': ['point_of_sale'], + 'depends': ['adyen_platforms', 'point_of_sale'], 'qweb': ['static/src/xml/pos.xml'], 'installable': True, - 'license': 'OEEL-1', } diff --git a/addons/pos_adyen/models/__init__.py b/addons/pos_adyen/models/__init__.py index 77e4db1fdaf..d8557bb1c89 100644 --- a/addons/pos_adyen/models/__init__.py +++ b/addons/pos_adyen/models/__init__.py @@ -1,5 +1,7 @@ # coding: utf-8 # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import adyen_account from . import pos_config from . import pos_payment_method +from . import res_config_settings diff --git a/addons/pos_adyen/models/adyen_account.py b/addons/pos_adyen/models/adyen_account.py new file mode 100644 index 00000000000..2db95110ccb --- /dev/null +++ b/addons/pos_adyen/models/adyen_account.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import uuid +from werkzeug.urls import url_join + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class AdyenAccount(models.Model): + _inherit = 'adyen.account' + + store_ids = fields.One2many('adyen.store', 'adyen_account_id') + terminal_ids = fields.One2many('adyen.terminal', 'adyen_account_id') + + def action_order_terminal(self): + if not self.store_ids: + raise ValidationError(_('Please create a store first.')) + + store_uuids = ','.join(self.store_ids.mapped('store_uuid')) + onboarding_url = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.onboarding_url') + return { + 'type': 'ir.actions.act_url', + 'target': 'new', + 'url': url_join(onboarding_url, 'order_terminals?store_uuids=%s' % store_uuids), + } + + +class AdyenStore(models.Model): + _name = 'adyen.store' + _inherit = ['adyen.address.mixin'] + _description = 'Adyen for Plaforms Store' + + adyen_account_id = fields.Many2one('adyen.account', ondelete='cascade') + store_reference = fields.Char('Reference', default=lambda self: uuid.uuid4().hex) + store_uuid = fields.Char('UUID', readonly=True) # Given by Adyen + name = fields.Char('Name', required=True) + phone_number = fields.Char('Phone Number', required=True) + terminal_ids = fields.One2many('adyen.terminal', 'store_id', string='Payment Terminals', readonly=True) + + @api.model + def create(self, values): + adyen_store_id = super(AdyenStore, self).create(values) + response = adyen_store_id.adyen_account_id._adyen_rpc('update_account_holder', adyen_store_id._format_data()) + stores = response['accountHolderDetails']['storeDetails'] + created_store = next(store for store in stores if store['storeReference'] == adyen_store_id.store_reference) + adyen_store_id.with_context(update_from_adyen=True).sudo().write({ + 'store_uuid': created_store['store'], + }) + return adyen_store_id + + def unlink(self): + for store_id in self: + store_id.adyen_account_id._adyen_rpc('close_stores', { + 'accountHolderCode': store_id.adyen_account_id.account_holder_code, + 'stores': [store_id.store_uuid], + }) + return super(AdyenStore, self).unlink() + + def _format_data(self): + return { + 'accountHolderCode': self.adyen_account_id.account_holder_code, + 'accountHolderDetails': { + 'storeDetails': [{ + 'storeReference': self.store_reference, + 'storeName': self.name, + 'merchantAccount': 'OdooMP_POS', + 'merchantCategoryCode': '7999', + 'address': { + 'city': self.city, + 'country': self.country_id.code, + 'houseNumberOrName': self.house_number_or_name, + 'postalCode': self.zip, + 'stateOrProvince': self.state_id.code or None, + 'street': self.street, + }, + 'fullPhoneNumber': self.phone_number, + }], + } + } + + +class AdyenTerminal(models.Model): + _name = 'adyen.terminal' + _description = 'Adyen for Plaforms Terminal' + _rec_name = 'terminal_uuid' + + adyen_account_id = fields.Many2one('adyen.account', ondelete='cascade') + store_id = fields.Many2one('adyen.store') + terminal_uuid = fields.Char('Terminal ID') + + @api.model + def _sync_adyen_terminals(self): + for adyen_store_id in self.env['adyen.store'].search([]): + response = adyen_store_id.adyen_account_id._adyen_rpc('connected_terminals', { + 'store': adyen_store_id.store_uuid, + }) + terminals_in_db = set(self.search([('store_id', '=', adyen_store_id.id)]).mapped('terminal_uuid')) + + # Added terminals + for terminal in set(response.get('uniqueTerminalIds')) - terminals_in_db: + self.sudo().create({ + 'adyen_account_id': adyen_store_id.adyen_account_id.id, + 'store_id': adyen_store_id.id, + 'terminal_uuid': terminal, + }) diff --git a/addons/pos_adyen/models/pos_payment_method.py b/addons/pos_adyen/models/pos_payment_method.py index ddc0ab395b9..73c09287bb3 100644 --- a/addons/pos_adyen/models/pos_payment_method.py +++ b/addons/pos_adyen/models/pos_payment_method.py @@ -6,6 +6,7 @@ import pprint import random import requests import string +from werkzeug.exceptions import Forbidden from odoo import fields, models, api, _ from odoo.exceptions import ValidationError @@ -16,11 +17,18 @@ class PosPaymentMethod(models.Model): _inherit = 'pos.payment.method' def _get_payment_terminal_selection(self): - return super(PosPaymentMethod, self)._get_payment_terminal_selection() + [('adyen', 'Adyen')] + return super(PosPaymentMethod, self)._get_payment_terminal_selection() + [('odoo_adyen', 'Odoo Payments by Adyen'), ('adyen', 'Adyen')] + # Adyen adyen_api_key = fields.Char(string="Adyen API key", help='Used when connecting to Adyen: https://docs.adyen.com/user-management/how-to-get-the-api-key/#description', copy=False) adyen_terminal_identifier = fields.Char(help='[Terminal model]-[Serial number], for example: P400Plus-123456789', copy=False) adyen_test_mode = fields.Boolean(help='Run transactions in the test environment.') + + # Odoo Payments by Adyen + adyen_account_id = fields.Many2one('adyen.account', related='company_id.adyen_account_id') + adyen_payout_id = fields.Many2one('adyen.payout', string='Adyen Payout', domain="[('adyen_account_id', '=', adyen_account_id)]") + adyen_terminal_id = fields.Many2one('adyen.terminal', string='Adyen Terminal', domain="[('adyen_account_id', '=', adyen_account_id)]") + adyen_latest_response = fields.Char(help='Technical field used to buffer the latest asynchronous notification from Adyen.', copy=False, groups='base.group_erp_manager') adyen_latest_diagnosis = fields.Char(help='Technical field used to determine if the terminal is still connected.', copy=False, groups='base.group_erp_manager') @@ -36,6 +44,17 @@ class PosPaymentMethod(models.Model): raise ValidationError(_('Terminal %s is already used on payment method %s.') % (payment_method.adyen_terminal_identifier, existing_payment_method.display_name)) + def _get_adyen_endpoints(self): + return { + 'terminal_request': 'https://terminal-api-%s.adyen.com/async', + } + + @api.onchange('adyen_terminal_id') + def onchange_use_payment_terminal(self): + for payment_method in self: + if payment_method.use_payment_terminal == 'odoo_adyen' and payment_method.adyen_terminal_id: + payment_method.adyen_terminal_identifier = payment_method.adyen_terminal_id.terminal_uuid + def _is_write_forbidden(self, fields): whitelisted_fields = set(('adyen_latest_response', 'adyen_latest_diagnosis')) return super(PosPaymentMethod, self)._is_write_forbidden(fields - whitelisted_fields) @@ -70,34 +89,35 @@ class PosPaymentMethod(models.Model): latest_response = self.sudo().adyen_latest_response latest_response = json.loads(latest_response) if latest_response else False - self.adyen_latest_response = '' # avoid handling old responses multiple times + self.sudo().adyen_latest_response = '' # avoid handling old responses multiple times return { 'latest_response': latest_response, - 'last_received_diagnosis_id': self.adyen_latest_diagnosis, + 'last_received_diagnosis_id': self.sudo().adyen_latest_diagnosis, } - def proxy_adyen_request(self, data, test_endpoint=False, live_endpoint=False): + def proxy_adyen_request(self, data, operation=False): ''' Necessary because Adyen's endpoints don't have CORS enabled ''' - self.ensure_one() - TIMEOUT = 10 + if not operation: + operation = 'terminal_request' - if not test_endpoint: - test_endpoint = 'https://terminal-api-test.adyen.com/async' - if not live_endpoint: - live_endpoint = 'https://terminal-api-live.adyen.com/async' + if self.use_payment_terminal == 'odoo_adyen': + return self._proxy_adyen_request_odoo_proxy(data, operation) + else: + return self._proxy_adyen_request_direct(data, operation) - endpoint = live_endpoint - if test_mode: - endpoint = test_endpoint + def _proxy_adyen_request_direct(self, data, operation): + self.ensure_one() + TIMEOUT = 10 _logger.info('request to adyen\n%s', pprint.pformat(data)) + + environment = 'test' if self.adyen_test_mode else 'live' + endpoint = self._get_adyen_endpoints()[operation] % environment headers = { 'x-api-key': self.adyen_api_key, - 'Content-Type': 'application/json' } - req = requests.post(endpoint, data=json.dumps(data), headers=headers, timeout=TIMEOUT) - _logger.info('response from adyen (HTTP status %s):\n%s', req.status_code, req.text) + req = requests.post(endpoint, json=data, headers=headers, timeout=TIMEOUT) # Authentication error doesn't return JSON if req.status_code == 401: @@ -113,9 +133,16 @@ class PosPaymentMethod(models.Model): return req.json() - @api.onchange('use_payment_terminal') - def _onchange_use_payment_terminal(self): - super(PosPaymentMethod, self)._onchange_use_payment_terminal() - if self.use_payment_terminal != 'adyen': - self.adyen_api_key = False - self.adyen_terminal_identifier = False + def _proxy_adyen_request_odoo_proxy(self, data, operation): + try: + return self.env.company.sudo().adyen_account_id._adyen_rpc(operation, { + 'request_data': data, + 'account_code': self.sudo().adyen_payout_id.code, + 'notification_url': self.env['ir.config_parameter'].sudo().get_param('web.base.url'), + }) + except Forbidden: + return { + 'error': { + 'status_code': 401, + } + } diff --git a/addons/pos_adyen/models/res_config_settings.py b/addons/pos_adyen/models/res_config_settings.py new file mode 100644 index 00000000000..4e3b7102c94 --- /dev/null +++ b/addons/pos_adyen/models/res_config_settings.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + adyen_account_id = fields.Many2one(string='Adyen Account', related='company_id.adyen_account_id') + + def create_adyen_account(self): + return self.env['adyen.account'].action_create_redirect() diff --git a/addons/pos_adyen/security/ir.model.access.csv b/addons/pos_adyen/security/ir.model.access.csv new file mode 100644 index 00000000000..302878ed368 --- /dev/null +++ b/addons/pos_adyen/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_adyen_store_group_erp_manager,adyen.store,model_adyen_store,base.group_erp_manager,1,0,1,1 +access_adyen_payout_group_pos_manager,adyen.payout,adyen_platforms.model_adyen_payout,point_of_sale.group_pos_manager,1,0,0,0 +access_adyen_terminal_group_pos_manager,adyen.terminal,model_adyen_terminal,point_of_sale.group_pos_manager,1,0,0,0 diff --git a/addons/pos_adyen/static/src/js/models.js b/addons/pos_adyen/static/src/js/models.js index f2734e00a94..c022ea7ba2b 100644 --- a/addons/pos_adyen/static/src/js/models.js +++ b/addons/pos_adyen/static/src/js/models.js @@ -3,5 +3,6 @@ var models = require('point_of_sale.models'); var PaymentAdyen = require('pos_adyen.payment'); models.register_payment_method('adyen', PaymentAdyen); +models.register_payment_method('odoo_adyen', PaymentAdyen); models.load_fields('pos.payment.method', 'adyen_terminal_identifier'); }); diff --git a/addons/pos_adyen/static/src/js/payment_adyen.js b/addons/pos_adyen/static/src/js/payment_adyen.js index 82c6a9f91ea..7e0350f8328 100644 --- a/addons/pos_adyen/static/src/js/payment_adyen.js +++ b/addons/pos_adyen/static/src/js/payment_adyen.js @@ -43,12 +43,11 @@ var PaymentAdyen = PaymentInterface.extend({ return Promise.reject(data); // prevent subsequent onFullFilled's from being called }, - _call_adyen: function (data, test_endpoint, live_endpoint) { - var self = this; + _call_adyen: function (data, operation) { return rpc.query({ model: 'pos.payment.method', method: 'proxy_adyen_request', - args: [[this.payment_method.id], data, test_endpoint, live_endpoint], + args: [[this.payment_method.id], data, operation], }, { // When a payment terminal is disconnected it takes Adyen // a while to return an error (~6s). So wait 10 seconds @@ -121,6 +120,7 @@ var PaymentAdyen = PaymentInterface.extend({ }, _adyen_cancel: function (ignore_error) { + var self = this; var previous_service_id = this.most_recent_service_id; var header = _.extend(this._adyen_common_message_header(), { 'MessageCategory': 'Abort', @@ -133,7 +133,6 @@ var PaymentAdyen = PaymentInterface.extend({ 'AbortReason': 'MerchantAbort', 'MessageReference': { 'MessageCategory': 'Payment', - 'SaleID': header.SaleID, 'ServiceID': previous_service_id, } }, @@ -144,7 +143,7 @@ var PaymentAdyen = PaymentInterface.extend({ // Only valid response is a 200 OK HTTP response which is // represented by true. - if (! ignore_error && data !== true) { + if (! ignore_error && data !== "ok") { self._show_error(_('Cancelling the payment failed. Please cancel it manually on the payment terminal.')); } }); @@ -238,7 +237,7 @@ var PaymentAdyen = PaymentInterface.extend({ if (message.startsWith('108 ')) { resolve(false); } else { - line.set_payment_status('force_done'); + line.set_payment_status('retry'); reject(); } } diff --git a/addons/pos_adyen/views/adyen_account_views.xml b/addons/pos_adyen/views/adyen_account_views.xml new file mode 100644 index 00000000000..c31771d9d97 --- /dev/null +++ b/addons/pos_adyen/views/adyen_account_views.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="adyen_account_view_form" model="ir.ui.view"> + <field name="name">adyen.account.view.form.inherit.pos.adyen</field> + <field name="model">adyen.account</field> + <field name="inherit_id" ref="adyen_platforms.adyen_account_view_form"/> + <field name="arch" type="xml"> + <xpath expr="//header" position="inside"> + <button name="action_order_terminal" string="Order a Terminal" class="oe_highlight" type="object" attrs="{'invisible': [('adyen_uuid', '=', False)]}"/> + </xpath> + <xpath expr="//notebook" position="inside"> + <page string="Stores"> + <field name="store_ids"> + <tree> + <field name="name"/> + <field name="store_uuid"/> + </tree> + </field> + </page> + </xpath> + </field> + </record> + + + <record id="adyen_store_view_form" model="ir.ui.view"> + <field name="name">adyen.store.view.form</field> + <field name="model">adyen.store</field> + <field name="arch" type="xml"> + <form string="Adyen Store"> + <sheet> + <group> + <field name="store_uuid" attrs="{'invisible': [('id', '=', False)]}"/> + <field name="name" attrs="{'readonly': [('id', '!=', False)]}"/> + <field name="phone_number" attrs="{'readonly': [('id', '!=', False)]}"/> + <label for="street" string="Address"/> + <div class="o_address_format"> + <field name="street" placeholder="Street" class="o_address_street" attrs="{'readonly': [('id', '!=', False)]}"/> + <field name="house_number_or_name" placeholder="House number or name" class="o_address_street" attrs="{'readonly': [('id', '!=', False)]}"/> + <field name="city" placeholder="City" class="o_address_city" attrs="{'readonly': [('id', '!=', False)]}"/> + <field name="state_id" class="o_address_state" placeholder="State" options="{'no_open': True, 'no_quick_create': True}" + attrs="{'required': [('country_code', 'in', ['AU', 'CA', 'IT', 'US'])], 'readonly': [('id', '!=', False)]}"/> + <field name="zip" placeholder="ZIP" class="o_address_zip" attrs="{'readonly': [('id', '!=', False)]}"/> + <field name="country_id" placeholder="Country" class="o_address_country" options="{'no_open': True, 'no_create': True}" attrs="{'readonly': [('id', '!=', False)]}"/> + <field name="country_code" invisible="1"/> + <field name="terminal_ids" attrs="{'invisible': [('terminal_ids', '=', [])]}"/> + </div> + </group> + </sheet> + </form> + </field> + </record> + + <record id="action_pos_adyen_account" model="ir.actions.server"> + <field name="name">Adyen Account</field> + <field name="model_id" ref="model_adyen_account"/> + <field name="state">code</field> + <field name="code"> + action = model.action_create_redirect() + </field> + </record> + + <menuitem id="menu_pos_adyen_account" + parent="point_of_sale.menu_point_config_product" + action="action_pos_adyen_account" + sequence="5" + groups="point_of_sale.group_pos_manager,point_of_sale.group_pos_user"/> + +</odoo> diff --git a/addons/pos_adyen/views/pos_payment_method_views.xml b/addons/pos_adyen/views/pos_payment_method_views.xml index 7c1d28597f0..fe6ce37dae2 100644 --- a/addons/pos_adyen/views/pos_payment_method_views.xml +++ b/addons/pos_adyen/views/pos_payment_method_views.xml @@ -1,16 +1,21 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> <record id="pos_payment_method_view_form_inherit_pos_adyen" model="ir.ui.view"> - <field name="name">pos.payment.method.form.inherit.adyen</field> - <field name="model">pos.payment.method</field> - <field name="inherit_id" ref="point_of_sale.pos_payment_method_view_form"/> - <field name="arch" type="xml"> - <xpath expr="//field[@name='use_payment_terminal']" position="after"> - <field name="adyen_api_key" - attrs="{'invisible': [('use_payment_terminal', '!=', 'adyen')], 'required': [('use_payment_terminal', '=', 'adyen')]}"/> + <field name="name">pos.payment.method.form.inherit.adyen</field> + <field name="model">pos.payment.method</field> + <field name="inherit_id" ref="point_of_sale.pos_payment_method_view_form"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='use_payment_terminal']" position="after"> + <!-- Adyen --> + <field name="adyen_api_key" attrs="{'invisible': [('use_payment_terminal', '!=', 'adyen')], 'required': [('use_payment_terminal', '=', 'adyen')]}"/> <field name="adyen_terminal_identifier" attrs="{'invisible': [('use_payment_terminal', '!=', 'adyen')], 'required': [('use_payment_terminal', '=', 'adyen')]}"/> - <field name="adyen_test_mode" attrs="{'invisible': [('use_payment_terminal', '!=', 'adyen')]}"/> - </xpath> - </field> + <field name="adyen_test_mode" attrs="{'invisible': [('use_payment_terminal', '!=', 'adyen')], 'required': [('use_payment_terminal', '=', 'adyen')]}"/> + + <!-- Odoo Payments by Adyen --> + <field name="adyen_account_id" invisible="1"/> + <field name="adyen_terminal_id" attrs="{'invisible': [('use_payment_terminal', '!=', 'odoo_adyen')], 'required': [('use_payment_terminal', '=', 'odoo_adyen')]}"/> + <field name="adyen_payout_id" attrs="{'invisible': [('use_payment_terminal', '!=', 'odoo_adyen')], 'required': [('use_payment_terminal', '=', 'odoo_adyen')]}" options="{'no_create': True}"/> + </xpath> + </field> </record> </odoo> diff --git a/addons/pos_adyen/views/res_config_settings_views.xml b/addons/pos_adyen/views/res_config_settings_views.xml new file mode 100644 index 00000000000..4d9995072ef --- /dev/null +++ b/addons/pos_adyen/views/res_config_settings_views.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="res_config_settings_view_form" model="ir.ui.view"> + <field name="name">res.config.settings.view.form.inherit.point_of_sale</field> + <field name="model">res.config.settings</field> + <field name="priority" eval="95"/> + <field name="inherit_id" ref="base.res_config_settings_view_form" /> + <field name="arch" type="xml"> + <xpath expr="//div[@id='adyen_payment_terminal_setting']/div[hasclass('o_setting_right_pane')]" position="inside"> + <span attrs="{'invisible': [('adyen_account_id', '=', False)]}"> + <label for="adyen_account_id" string="Account"/> + <span class="fa fa-lg fa-building-o mr8" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/> + <field name="adyen_account_id"/> + </span> + <button name="create_adyen_account" icon="fa-arrow-right" type="object" class="btn-link" string="Create an account in 1 minute" + attrs="{'invisible': [('adyen_account_id', '!=', False)]}"/> + </xpath> + </field> + </record> +</odoo> |
