summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/adyen_platforms/__init__.py5
-rw-r--r--addons/adyen_platforms/__manifest__.py24
-rw-r--r--addons/adyen_platforms/controllers/__init__.py4
-rw-r--r--addons/adyen_platforms/controllers/adyen_platforms.py13
-rw-r--r--addons/adyen_platforms/data/adyen_platforms_data.xml26
-rw-r--r--addons/adyen_platforms/models/__init__.py6
-rw-r--r--addons/adyen_platforms/models/adyen_account.py711
-rw-r--r--addons/adyen_platforms/models/adyen_transaction.py83
-rw-r--r--addons/adyen_platforms/models/res_company.py10
-rw-r--r--addons/adyen_platforms/security/ir.model.access.csv6
-rw-r--r--addons/adyen_platforms/static/src/js/adyen_account_fields.js20
-rw-r--r--addons/adyen_platforms/static/src/js/adyen_account_views.js63
-rw-r--r--addons/adyen_platforms/static/src/js/adyen_transactions.js33
-rw-r--r--addons/adyen_platforms/static/src/xml/adyen_account_templates.xml125
-rw-r--r--addons/adyen_platforms/static/src/xml/adyen_transactions_templates.xml10
-rw-r--r--addons/adyen_platforms/util.py42
-rw-r--r--addons/adyen_platforms/views/adyen_account_templates.xml30
-rw-r--r--addons/adyen_platforms/views/adyen_account_views.xml243
-rw-r--r--addons/adyen_platforms/views/adyen_transaction_views.xml36
-rw-r--r--addons/adyen_platforms/views/assets.xml10
-rw-r--r--addons/point_of_sale/views/res_config_settings_views.xml26
-rw-r--r--addons/pos_adyen/__manifest__.py6
-rw-r--r--addons/pos_adyen/models/__init__.py2
-rw-r--r--addons/pos_adyen/models/adyen_account.py107
-rw-r--r--addons/pos_adyen/models/pos_payment_method.py71
-rw-r--r--addons/pos_adyen/models/res_config_settings.py13
-rw-r--r--addons/pos_adyen/security/ir.model.access.csv4
-rw-r--r--addons/pos_adyen/static/src/js/models.js1
-rw-r--r--addons/pos_adyen/static/src/js/payment_adyen.js11
-rw-r--r--addons/pos_adyen/views/adyen_account_views.xml68
-rw-r--r--addons/pos_adyen/views/pos_payment_method_views.xml25
-rw-r--r--addons/pos_adyen/views/res_config_settings_views.xml20
-rw-r--r--addons/pos_restaurant_adyen/models/pos_payment.py8
-rw-r--r--addons/pos_restaurant_adyen/models/pos_payment_method.py7
-rw-r--r--addons/pos_restaurant_adyen/static/src/js/payment_adyen.js6
35 files changed, 1810 insertions, 65 deletions
diff --git a/addons/adyen_platforms/__init__.py b/addons/adyen_platforms/__init__.py
new file mode 100644
index 00000000000..7d34c7c054a
--- /dev/null
+++ b/addons/adyen_platforms/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import controllers
+from . import models
diff --git a/addons/adyen_platforms/__manifest__.py b/addons/adyen_platforms/__manifest__.py
new file mode 100644
index 00000000000..189ff6b8204
--- /dev/null
+++ b/addons/adyen_platforms/__manifest__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'Adyen for Platforms',
+ 'version': '1.0',
+ 'category': '',
+ 'summary': 'Base Module for Adyen for Platforms',
+ 'description': 'Base Module for Adyen for Platforms, used in eCommerce and PoS',
+ 'depends': ['mail', 'web'],
+ 'data': [
+ 'data/adyen_platforms_data.xml',
+ 'security/ir.model.access.csv',
+ 'views/adyen_account_templates.xml',
+ 'views/adyen_account_views.xml',
+ 'views/adyen_transaction_views.xml',
+ 'views/assets.xml',
+ ],
+ 'qweb': [
+ "static/src/xml/adyen_account_templates.xml",
+ "static/src/xml/adyen_transactions_templates.xml",
+ ],
+ 'installable': True,
+}
diff --git a/addons/adyen_platforms/controllers/__init__.py b/addons/adyen_platforms/controllers/__init__.py
new file mode 100644
index 00000000000..7ea350e3385
--- /dev/null
+++ b/addons/adyen_platforms/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import adyen_platforms
diff --git a/addons/adyen_platforms/controllers/adyen_platforms.py b/addons/adyen_platforms/controllers/adyen_platforms.py
new file mode 100644
index 00000000000..a26ca88dfff
--- /dev/null
+++ b/addons/adyen_platforms/controllers/adyen_platforms.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import http
+from odoo.http import request
+
+
+class AdyenPlatformsController(http.Controller):
+
+ @http.route('/adyen_platforms/create_account', type='http', auth='user', website=True)
+ def adyen_platforms_create_account(self, creation_token):
+ request.session['adyen_creation_token'] = creation_token
+ return request.redirect('/web?#action=adyen_platforms.adyen_account_action_create')
diff --git a/addons/adyen_platforms/data/adyen_platforms_data.xml b/addons/adyen_platforms/data/adyen_platforms_data.xml
new file mode 100644
index 00000000000..c01578fe449
--- /dev/null
+++ b/addons/adyen_platforms/data/adyen_platforms_data.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data noupdate="1">
+ <record model="ir.config_parameter" id="adyen_platforms_proxy_url">
+ <field name="key">adyen_platforms.proxy_url</field>
+ <field name="value">https://payment-adyen.odoo.com/payment_proxy_adyen/</field>
+ </record>
+
+ <record model="ir.config_parameter" id="adyen_platforms_onboarding_url">
+ <field name="key">adyen_platforms.onboarding_url</field>
+ <field name="value">https://www.odoo.com/odoo_adyen/</field>
+ </record>
+
+ <record id="adyen_sync_cron" model="ir.cron">
+ <field name="name">Adyen Sync</field>
+ <field name="model_id" ref="model_adyen_account"/>
+ <field name="state">code</field>
+ <field name="code">model._sync_adyen_cron()</field>
+ <field name="user_id" ref="base.user_root"/>
+ <field name="interval_number">1</field>
+ <field name="interval_type">days</field>
+ <field name="numbercall">-1</field>
+ <field name="doall" eval="False"/>
+ </record>
+ </data>
+</odoo>
diff --git a/addons/adyen_platforms/models/__init__.py b/addons/adyen_platforms/models/__init__.py
new file mode 100644
index 00000000000..480ddb07be1
--- /dev/null
+++ b/addons/adyen_platforms/models/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import adyen_account
+from . import adyen_transaction
+from . import res_company
diff --git a/addons/adyen_platforms/models/adyen_account.py b/addons/adyen_platforms/models/adyen_account.py
new file mode 100644
index 00000000000..434f9f3bc06
--- /dev/null
+++ b/addons/adyen_platforms/models/adyen_account.py
@@ -0,0 +1,711 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import os
+import requests
+import uuid
+from werkzeug.urls import url_join
+
+from odoo import api, fields, models, _
+from odoo.http import request
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools import date_utils
+
+from odoo.addons.adyen_platforms.util import AdyenProxyAuth
+
+ADYEN_AVAILABLE_COUNTRIES = ['US', 'AT', 'AU', 'BE', 'CA', 'CH', 'CZ', 'DE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'IE', 'IT', 'LT', 'LU', 'NL', 'PL', 'PT']
+TIMEOUT = 60
+
+
+class AdyenAddressMixin(models.AbstractModel):
+ _name = 'adyen.address.mixin'
+ _description = 'Adyen for Plaforms Address Mixin'
+
+ country_id = fields.Many2one('res.country', string='Country', domain=[('code', 'in', ADYEN_AVAILABLE_COUNTRIES)], required=True)
+ country_code = fields.Char(related='country_id.code')
+ state_id = fields.Many2one('res.country.state', string='State', domain="[('country_id', '=?', country_id)]")
+ state_code = fields.Char(related='state_id.code')
+ city = fields.Char('City', required=True)
+ zip = fields.Char('ZIP', required=True)
+ street = fields.Char('Street', required=True)
+ house_number_or_name = fields.Char('House Number Or Name', required=True)
+
+
+class AdyenIDMixin(models.AbstractModel):
+ _name = 'adyen.id.mixin'
+ _description = 'Adyen for Platforms ID Mixin'
+
+ id_type = fields.Selection(string='Photo ID type', selection=[
+ ('PASSPORT', 'Passport'),
+ ('ID_CARD', 'ID Card'),
+ ('DRIVING_LICENSE', 'Driving License'),
+ ])
+ id_front = fields.Binary('Photo ID Front', help="Allowed formats: jpg, pdf, png. Maximum allowed size: 4MB.")
+ id_front_filename = fields.Char()
+ id_back = fields.Binary('Photo ID Back', help="Allowed formats: jpg, pdf, png. Maximum allowed size: 4MB.")
+ id_back_filename = fields.Char()
+
+ def write(self, vals):
+ res = super(AdyenIDMixin, self).write(vals)
+ if vals.get('id_front'):
+ document_type = self.id_type
+ if self.id_type in ['ID_CARD', 'DRIVING_LICENSE']:
+ document_type += '_FRONT'
+ self._check_file_requirements(self.id_front, self.id_front_filename)
+ self._upload_photo_id(document_type, self.id_front, self.id_front_filename)
+ if vals.get('id_back'):
+ document_type = self.id_type + '_BACK'
+ self._check_file_requirements(self.id_back, self.id_back_filename)
+ self._upload_photo_id(document_type, self.id_back, self.id_back_filename)
+ return res
+
+ def _check_file_requirements(self, content, filename):
+ file_extension = os.path.splitext(filename)[1]
+ file_size = len(content)
+ if file_extension not in ['.jpeg', '.jpg', '.pdf', '.png']:
+ raise ValidationError(_('Allowed file formats for photo IDs are jpeg, jpg, pdf or png'))
+ if file_size >> 20 > 4 or (file_size >> 10 < 1 and file_extension == '.pdf') or (file_size >> 10 < 100 and file_extension != '.pdf') :
+ raise ValidationError(_('Photo ID file size must be between 100kB (1kB for PDFs) and 4MB'))
+
+ def _upload_photo_id(self, document_type, content, filename):
+ # The request to be sent to Adyen will be different for Individuals,
+ # Shareholders, etc. This method should be implemented by the models
+ # inheriting this mixin
+ raise NotImplementedError()
+
+
+class AdyenAccount(models.Model):
+ _name = 'adyen.account'
+ _inherit = ['mail.thread', 'adyen.id.mixin', 'adyen.address.mixin']
+
+ _description = 'Adyen for Plaforms Account'
+ _rec_name = 'full_name'
+
+ # Credentials
+ proxy_token = fields.Char('Proxy Token')
+ adyen_uuid = fields.Char('Adyen UUID')
+ account_holder_code = fields.Char('Account Holder Code', default=lambda self: uuid.uuid4().hex)
+
+ company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
+ payout_ids = fields.One2many('adyen.payout', 'adyen_account_id', string='Payouts')
+ shareholder_ids = fields.One2many('adyen.shareholder', 'adyen_account_id', string='Shareholders')
+ bank_account_ids = fields.One2many('adyen.bank.account', 'adyen_account_id', string='Bank Accounts')
+ transaction_ids = fields.One2many('adyen.transaction', 'adyen_account_id', string='Transactions')
+ transactions_count = fields.Integer(compute='_compute_transactions_count')
+
+ is_business = fields.Boolean('Is a business', required=True)
+
+ # Contact Info
+ full_name = fields.Char(compute='_compute_full_name')
+ email = fields.Char('Email', required=True)
+ phone_number = fields.Char('Phone Number', required=True)
+
+ # Individual
+ first_name = fields.Char('First Name')
+ last_name = fields.Char('Last Name')
+ date_of_birth = fields.Date('Date of birth')
+ document_number = fields.Char('ID Number',
+ help="The type of ID Number required depends on the country:\n"
+ "US: Social Security Number (9 digits or last 4 digits)\n"
+ "Canada: Social Insurance Number\nItaly: Codice fiscale\n"
+ "Australia: Document Number")
+ document_type = fields.Selection(string='Document Type', selection=[
+ ('ID', 'ID'),
+ ('PASSPORT', 'Passport'),
+ ('VISA', 'Visa'),
+ ('DRIVINGLICENSE', 'Driving license'),
+ ], default='ID')
+
+ # Business
+ legal_business_name = fields.Char('Legal Business Name')
+ doing_business_as = fields.Char('Doing Business As')
+ registration_number = fields.Char('Registration Number')
+
+ # KYC
+ kyc_status = fields.Selection(string='KYC Status', selection=[
+ ('awaiting_data', 'Data to provide'),
+ ('pending', 'Waiting for validation'),
+ ('passed', 'Confirmed'),
+ ('failed', 'Failed'),
+ ], required=True, default='pending')
+ kyc_status_message = fields.Char('KYC Status Message', readonly=True)
+
+ _sql_constraints = [
+ ('adyen_uuid_uniq', 'UNIQUE(adyen_uuid)', 'Adyen UUID should be unique'),
+ ]
+
+ @api.depends('transaction_ids')
+ def _compute_transactions_count(self):
+ for adyen_account_id in self:
+ adyen_account_id.transactions_count = len(adyen_account_id.transaction_ids)
+
+ @api.depends('first_name', 'last_name', 'legal_business_name')
+ def _compute_full_name(self):
+ for adyen_account_id in self:
+ if adyen_account_id.is_business:
+ adyen_account_id.full_name = adyen_account_id.legal_business_name
+ else:
+ adyen_account_id.full_name = "%s %s" % (adyen_account_id.first_name, adyen_account_id.last_name)
+
+ @api.model
+ def create(self, values):
+ adyen_account_id = super(AdyenAccount, self).create(values)
+ self.env.company.adyen_account_id = adyen_account_id.id
+
+ # Create account on odoo.com, proxy and Adyen
+ response = adyen_account_id._adyen_rpc('create_account_holder', adyen_account_id._format_data())
+
+ # Save adyen_uuid and proxy_token, that have been generated by odoo.com and the proxy
+ adyen_account_id.with_context(update_from_adyen=True).write({
+ 'adyen_uuid': response['adyen_uuid'],
+ 'proxy_token': response['proxy_token'],
+ })
+
+ # A default payout is created for all adyen accounts
+ adyen_account_id.env['adyen.payout'].with_context(update_from_adyen=True).create({
+ 'code': response['adyen_response']['accountCode'],
+ 'adyen_account_id': adyen_account_id.id,
+ })
+ return adyen_account_id
+
+ def write(self, vals):
+ res = super(AdyenAccount, self).write(vals)
+ if not self.env.context.get('update_from_adyen'):
+ self._adyen_rpc('update_account_holder', self._format_data())
+ return res
+
+ def unlink(self):
+ for adyen_account_id in self:
+ adyen_account_id._adyen_rpc('close_account_holder', {
+ 'accountHolderCode': adyen_account_id.account_holder_code,
+ })
+ return super(AdyenAccount, self).unlink()
+
+ @api.model
+ def action_create_redirect(self):
+ '''
+ Accessing the FormView to create an Adyen account needs to be done through this action.
+ The action will redirect the user to accounts.odoo.com to link an Odoo user_id to the Adyen
+ account. After logging in on odoo.com the user will be redirected to his DB with a token in
+ the URL. This token is then needed to create the Adyen account.
+ '''
+ if self.env.company.adyen_account_id:
+ # An account already exists, show it
+ return {
+ 'name': _('Adyen Account'),
+ 'view_mode': 'form',
+ 'res_model': 'adyen.account',
+ 'res_id': self.env.company.adyen_account_id.id,
+ 'type': 'ir.actions.act_window',
+ }
+ return_url = url_join(self.env['ir.config_parameter'].sudo().get_param('web.base.url'), 'adyen_platforms/create_account')
+ onboarding_url = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.onboarding_url')
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': url_join(onboarding_url, 'get_creation_token?return_url=%s' % return_url),
+ }
+
+ def action_show_transactions(self):
+ return {
+ 'name': _('Transactions'),
+ 'view_mode': 'tree,form',
+ 'domain': [('adyen_account_id', '=', self.id)],
+ 'res_model': 'adyen.transaction',
+ 'type': 'ir.actions.act_window',
+ 'context': {'group_by': ['adyen_payout_id']}
+ }
+
+ def _upload_photo_id(self, document_type, content, filename):
+ self._adyen_rpc('upload_document', {
+ 'documentDetail': {
+ 'accountHolderCode': self.account_holder_code,
+ 'documentType': document_type,
+ 'filename': filename,
+ },
+ 'documentContent': content,
+ })
+
+ def _format_data(self):
+ data = {
+ 'accountHolderCode': self.account_holder_code,
+ 'accountHolderDetails': {
+ 'address': {
+ 'country': self.country_id.code,
+ 'stateOrProvince': self.state_id.code or None,
+ 'city': self.city,
+ 'postalCode': self.zip,
+ 'street': self.street,
+ 'houseNumberOrName': self.house_number_or_name,
+ },
+ 'email': self.email,
+ 'fullPhoneNumber': self.phone_number,
+ },
+ 'legalEntity': 'Business' if self.is_business else 'Individual',
+ }
+
+ if self.is_business:
+ data['accountHolderDetails']['businessDetails'] = {
+ 'legalBusinessName': self.legal_business_name,
+ 'doingBusinessAs': self.doing_business_as,
+ 'registrationNumber': self.registration_number,
+ }
+ else:
+ data['accountHolderDetails']['individualDetails'] = {
+ 'name': {
+ 'firstName': self.first_name,
+ 'lastName': self.last_name,
+ 'gender': 'UNKNOWN',
+ },
+ 'personalData': {
+ 'dateOfBirth': str(self.date_of_birth),
+ }
+ }
+
+ # documentData cannot be present in the data if not set
+ if self.document_number:
+ data['accountHolderDetails']['individualDetails']['personalData']['documentData'] = [{
+ 'number': self.document_number,
+ 'type': self.document_type,
+ }]
+
+ return data
+
+ def _adyen_rpc(self, operation, adyen_data={}):
+ if operation == 'create_account_holder':
+ url = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.onboarding_url')
+ params = {
+ 'creation_token': request.session.get('adyen_creation_token'),
+ 'adyen_data': adyen_data,
+ }
+ auth = None
+ else:
+ url = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.proxy_url')
+ params = {
+ 'adyen_uuid': self.adyen_uuid,
+ 'adyen_data': adyen_data,
+ }
+ auth = AdyenProxyAuth(self)
+
+ payload = {
+ 'jsonrpc': '2.0',
+ 'params': params,
+ }
+ try:
+ req = requests.post(url_join(url, operation), json=payload, auth=auth, timeout=TIMEOUT)
+ req.raise_for_status()
+ except requests.exceptions.Timeout:
+ raise UserError(_('A timeout occured whil trying to reach the Adyen proxy.'))
+ except Exception as e:
+ raise UserError(_('The Adyen proxy is not reachable, please try again later.'))
+ response = req.json()
+
+ if 'error' in response:
+ name = response['error']['data'].get('name').rpartition('.')[-1]
+ if name == 'ValidationError':
+ raise ValidationError(response['error']['data'].get('arguments')[0])
+ else:
+ raise UserError(_("We had troubles reaching Adyen, please retry later or contact the support if the problem persists"))
+
+ return response.get('result')
+
+ @api.model
+ def _sync_adyen_cron(self):
+ self._sync_adyen_kyc_status()
+ self.env['adyen.transaction'].sync_adyen_transactions()
+ self.env['adyen.payout']._process_payouts()
+
+ @api.model
+ def _sync_adyen_kyc_status(self):
+ for adyen_account_id in self.search([]):
+ data = adyen_account_id._adyen_rpc('get_account_holder', {
+ 'accountHolderCode': adyen_account_id.account_holder_code,
+ })
+ adyen_account_id._update_kyc_status(data['verification'])
+
+ def _update_kyc_status(self, checks):
+ all_checks_status = []
+
+ # Account Holder Checks
+ account_holder_checks = checks.get('accountHolder', {})
+ account_holder_messages = []
+ for check in account_holder_checks.get('checks'):
+ all_checks_status.append(check['status'])
+ kyc_status_message = self._get_kyc_message(check)
+ if kyc_status_message:
+ account_holder_messages.append(kyc_status_message)
+
+ # Shareholders Checks
+ shareholder_checks = checks.get('shareholders', {})
+ shareholder_messages = []
+ kyc_status_message = False
+ for sc in shareholder_checks:
+ shareholder_status = []
+ shareholder_id = self.shareholder_ids.filtered(lambda shareholder: shareholder.shareholder_uuid == sc['shareholderCode'])
+ for check in sc.get('checks'):
+ all_checks_status.append(check['status'])
+ shareholder_status.append(check['status'])
+ kyc_status_message = self._get_kyc_message(check)
+ if kyc_status_message:
+ shareholder_messages.append('[%s] %s' % (shareholder_id.display_name, kyc_status_message))
+ shareholder_id.with_context(update_from_adyen=True).write({
+ 'kyc_status': self.get_status(shareholder_status),
+ 'kyc_status_message': kyc_status_message,
+ })
+
+ # Bank Account Checks
+ bank_account_checks = checks.get('bankAccounts', {})
+ bank_account_messages = []
+ kyc_status_message = False
+ for bac in bank_account_checks:
+ bank_account_status = []
+ bank_account_id = self.bank_account_ids.filtered(lambda bank_account: bank_account.bank_account_uuid == bac['bankAccountUUID'])
+ for check in bac.get('checks'):
+ all_checks_status.append(check['status'])
+ bank_account_status.append(check['status'])
+ kyc_status_message = self._get_kyc_message(check)
+ if kyc_status_message:
+ bank_account_messages.append('[%s] %s' % (bank_account_id.display_name, kyc_status_message))
+ bank_account_id.with_context(update_from_adyen=True).write({
+ 'kyc_status': self.get_status(bank_account_status),
+ 'kyc_status_message': kyc_status_message,
+ })
+
+ kyc_status = self.get_status(all_checks_status)
+ kyc_status_message = self.env['ir.qweb']._render('adyen_platforms.kyc_status_message', {
+ 'kyc_status': dict(self._fields['kyc_status'].selection)[kyc_status],
+ 'account_holder_messages': account_holder_messages,
+ 'shareholder_messages': shareholder_messages,
+ 'bank_account_messages': bank_account_messages,
+ })
+
+ if kyc_status_message.decode() != self.kyc_status_message:
+ self.sudo().message_post(body = kyc_status_message, subtype_xmlid="mail.mt_comment") # Message from Odoo Bot
+
+ self.with_context(update_from_adyen=True).write({
+ 'kyc_status': kyc_status,
+ 'kyc_status_message': kyc_status_message,
+ })
+
+ @api.model
+ def get_status(self, statuses):
+ if any(status in ['FAILED'] for status in statuses):
+ return 'failed'
+ if any(status in ['INVALID_DATA', 'RETRY_LIMIT_REACHED', 'AWAITING_DATA'] for status in statuses):
+ return 'awaiting_data'
+ if any(status in ['DATA_PROVIDED', 'PENDING'] for status in statuses):
+ return 'pending'
+ return 'passed'
+
+ @api.model
+ def _get_kyc_message(self, check):
+ if check.get('summary', {}).get('kycCheckDescription'):
+ return check['summary']['kycCheckDescription']
+ if check.get('requiredFields', {}):
+ return _('Missing required fields: ') + ', '.join(check.get('requiredFields'))
+ return ''
+
+
+class AdyenShareholder(models.Model):
+ _name = 'adyen.shareholder'
+ _inherit = ['adyen.id.mixin', 'adyen.address.mixin']
+ _description = 'Adyen for Plaforms Shareholder'
+ _rec_name = 'full_name'
+
+ adyen_account_id = fields.Many2one('adyen.account', ondelete='cascade')
+ shareholder_reference = fields.Char('Reference', default=lambda self: uuid.uuid4().hex)
+ shareholder_uuid = fields.Char('UUID') # Given by Adyen
+ first_name = fields.Char('First Name', required=True)
+ last_name = fields.Char('Last Name', required=True)
+ full_name = fields.Char(compute='_compute_full_name')
+ date_of_birth = fields.Date('Date of birth', required=True)
+ document_number = fields.Char('ID Number',
+ help="The type of ID Number required depends on the country:\n"
+ "US: Social Security Number (9 digits or last 4 digits)\n"
+ "Canada: Social Insurance Number\nItaly: Codice fiscale\n"
+ "Australia: Document Number")
+
+ # KYC
+ kyc_status = fields.Selection(string='KYC Status', selection=[
+ ('awaiting_data', 'Data to provide'),
+ ('pending', 'Waiting for validation'),
+ ('passed', 'Confirmed'),
+ ('failed', 'Failed'),
+ ], required=True, default='pending')
+ kyc_status_message = fields.Char('KYC Status Message', readonly=True)
+
+ @api.depends('first_name', 'last_name')
+ def _compute_full_name(self):
+ for adyen_shareholder_id in self:
+ adyen_shareholder_id.full_name = '%s %s' % (adyen_shareholder_id.first_name, adyen_shareholder_id.last_name)
+
+ @api.model
+ def create(self, values):
+ adyen_shareholder_id = super(AdyenShareholder, self).create(values)
+ response = adyen_shareholder_id.adyen_account_id._adyen_rpc('update_account_holder', adyen_shareholder_id._format_data())
+ shareholders = response['accountHolderDetails']['businessDetails']['shareholders']
+ created_shareholder = next(shareholder for shareholder in shareholders if shareholder['shareholderReference'] == adyen_shareholder_id.shareholder_reference)
+ adyen_shareholder_id.with_context(update_from_adyen=True).write({
+ 'shareholder_uuid': created_shareholder['shareholderCode'],
+ })
+ return adyen_shareholder_id
+
+ def write(self, vals):
+ res = super(AdyenShareholder, self).write(vals)
+ if not self.env.context.get('update_from_adyen'):
+ self.adyen_account_id._adyen_rpc('update_account_holder', self._format_data())
+ return res
+
+ def unlink(self):
+ for shareholder_id in self:
+ shareholder_id.adyen_account_id._adyen_rpc('delete_shareholders', {
+ 'accountHolderCode': shareholder_id.adyen_account_id.account_holder_code,
+ 'shareholderCodes': [shareholder_id.shareholder_uuid],
+ })
+ return super(AdyenShareholder, self).unlink()
+
+ def _upload_photo_id(self, document_type, content, filename):
+ self.adyen_account_id._adyen_rpc('upload_document', {
+ 'documentDetail': {
+ 'accountHolderCode': self.adyen_account_id.account_holder_code,
+ 'shareholderCode': self.shareholder_uuid,
+ 'documentType': document_type,
+ 'filename': filename,
+ },
+ 'documentContent': content,
+ })
+
+ def _format_data(self):
+ data = {
+ 'accountHolderCode': self.adyen_account_id.account_holder_code,
+ 'accountHolderDetails': {
+ 'businessDetails': {
+ 'shareholders': [{
+ 'shareholderCode': self.shareholder_uuid or None,
+ 'shareholderReference': self.shareholder_reference,
+ 'address': {
+ 'city': self.city,
+ 'country': self.country_code,
+ 'houseNumberOrName': self.house_number_or_name,
+ 'postalCode': self.zip,
+ 'stateOrProvince': self.state_id.code or None,
+ 'street': self.street,
+ },
+ 'name': {
+ 'firstName': self.first_name,
+ 'lastName': self.last_name,
+ 'gender': 'UNKNOWN'
+ },
+ 'personalData': {
+ 'dateOfBirth': str(self.date_of_birth),
+ }
+ }]
+ }
+ }
+ }
+
+ # documentData cannot be present in the data if not set
+ if self.document_number:
+ data['accountHolderDetails']['businessDetails']['shareholders'][0]['personalData']['documentData'] = [{
+ 'number': self.document_number,
+ 'type': 'ID',
+ }]
+
+ return data
+
+class AdyenBankAccount(models.Model):
+ _name = 'adyen.bank.account'
+ _description = 'Adyen for Plaforms Bank Account'
+
+ adyen_account_id = fields.Many2one('adyen.account', ondelete='cascade')
+ bank_account_reference = fields.Char('Reference', default=lambda self: uuid.uuid4().hex)
+ bank_account_uuid = fields.Char('UUID') # Given by Adyen
+ owner_name = fields.Char('Owner Name', required=True)
+ country_id = fields.Many2one('res.country', string='Country', domain=[('code', 'in', ADYEN_AVAILABLE_COUNTRIES)], required=True)
+ country_code = fields.Char(related='country_id.code')
+ currency_id = fields.Many2one('res.currency', string='Currency', required=True)
+ iban = fields.Char('IBAN')
+ account_number = fields.Char('Account Number')
+ branch_code = fields.Char('Branch Code')
+ bank_code = fields.Char('Bank Code')
+ account_type = fields.Selection(string='Account Type', selection=[
+ ('checking', 'Checking'),
+ ('savings', 'Savings'),
+ ])
+ owner_country_id = fields.Many2one('res.country', string='Owner Country')
+ owner_state_id = fields.Many2one('res.country.state', 'Owner State', domain="[('country_id', '=?', owner_country_id)]")
+ owner_street = fields.Char('Owner Street')
+ owner_city = fields.Char('Owner City')
+ owner_zip = fields.Char('Owner ZIP')
+ owner_house_number_or_name = fields.Char('Owner House Number or Name')
+
+ bank_statement = fields.Binary('Bank Statement', help="You need to provide a bank statement to allow payouts. \
+ The file must be a bank statement, a screenshot of your online banking environment, a letter from the bank or a cheque and must contain \
+ the logo of the bank or it's name in a unique font, the bank account details, the name of the account holder.\
+ Allowed formats: jpg, pdf, png. Maximum allowed size: 10MB.")
+ bank_statement_filename = fields.Char()
+
+ # KYC
+ kyc_status = fields.Selection(string='KYC Status', selection=[
+ ('awaiting_data', 'Data to provide'),
+ ('pending', 'Waiting for validation'),
+ ('passed', 'Confirmed'),
+ ('failed', 'Failed'),
+ ], required=True, default='pending')
+ kyc_status_message = fields.Char('KYC Status Message', readonly=True)
+
+ @api.model
+ def create(self, values):
+ adyen_bank_account_id = super(AdyenBankAccount, self).create(values)
+ response = adyen_bank_account_id.adyen_account_id._adyen_rpc('update_account_holder', adyen_bank_account_id._format_data())
+ bank_accounts = response['accountHolderDetails']['bankAccountDetails']
+ created_bank_account = next(bank_account for bank_account in bank_accounts if bank_account['bankAccountReference'] == adyen_bank_account_id.bank_account_reference)
+ adyen_bank_account_id.with_context(update_from_adyen=True).write({
+ 'bank_account_uuid': created_bank_account['bankAccountUUID'],
+ })
+ return adyen_bank_account_id
+
+ def write(self, vals):
+ res = super(AdyenBankAccount, self).write(vals)
+ if not self.env.context.get('update_from_adyen'):
+ self.adyen_account_id._adyen_rpc('update_account_holder', self._format_data())
+ if 'bank_statement' in vals:
+ self._upload_bank_statement(vals['bank_statement'], vals['bank_statement_filename'])
+ return res
+
+ def unlink(self):
+ for bank_account_id in self:
+ bank_account_id.adyen_account_id._adyen_rpc('delete_bank_accounts', {
+ 'accountHolderCode': bank_account_id.adyen_account_id.account_holder_code,
+ 'bankAccountUUIDs': [bank_account_id.bank_account_uuid],
+ })
+ return super(AdyenBankAccount, self).unlink()
+
+ def _format_data(self):
+ return {
+ 'accountHolderCode': self.adyen_account_id.account_holder_code,
+ 'accountHolderDetails': {
+ 'bankAccountDetails': [{
+ 'accountNumber': self.account_number or None,
+ 'accountType': self.account_type or None,
+ 'bankAccountReference': self.bank_account_reference,
+ 'bankAccountUUID': self.bank_account_uuid or None,
+ 'bankCode': self.bank_code or None,
+ 'branchCode': self.branch_code or None,
+ 'countryCode': self.country_code,
+ 'currencyCode': self.currency_id.name,
+ 'iban': self.iban or None,
+ 'ownerCity': self.owner_city or None,
+ 'ownerCountryCode': self.owner_country_id.code or None,
+ 'ownerHouseNumberOrName': self.owner_house_number_or_name or None,
+ 'ownerName': self.owner_name,
+ 'ownerPostalCode': self.owner_zip or None,
+ 'ownerState': self.owner_state_id.code or None,
+ 'ownerStreet': self.owner_street or None,
+ }],
+ }
+ }
+
+ def _upload_bank_statement(self, content, filename):
+ file_extension = os.path.splitext(filename)[1]
+ file_size = len(content.encode('utf-8'))
+ if file_extension not in ['.jpeg', '.jpg', '.pdf', '.png']:
+ raise ValidationError(_('Allowed file formats for bank statements are jpeg, jpg, pdf or png'))
+ if file_size >> 20 > 10 or (file_size >> 10 < 10 and file_extension != '.pdf') :
+ raise ValidationError(_('Bank statements must be greater than 10kB (except for PDFs) and smaller than 10MB'))
+
+ self.adyen_account_id._adyen_rpc('upload_document', {
+ 'documentDetail': {
+ 'accountHolderCode': self.adyen_account_id.account_holder_code,
+ 'bankAccountUUID': self.bank_account_uuid,
+ 'documentType': 'BANK_STATEMENT',
+ 'filename': filename,
+ },
+ 'documentContent': content,
+ })
+
+
+class AdyenPayout(models.Model):
+ _name = 'adyen.payout'
+ _description = 'Adyen for Plaforms Payout'
+
+ @api.depends('payout_schedule')
+ def _compute_next_scheduled_payout(self):
+ today = fields.date.today()
+ for adyen_payout_id in self:
+ adyen_payout_id.next_scheduled_payout = date_utils.end_of(today, adyen_payout_id.payout_schedule)
+
+ adyen_account_id = fields.Many2one('adyen.account', ondelete='cascade')
+ adyen_bank_account_id = fields.Many2one('adyen.bank.account', string='Bank Account',
+ help='The bank account to which the payout is to be made. If left blank, a bank account is automatically selected')
+ name = fields.Char('Name', default='Default', required=True)
+ code = fields.Char('Account Code')
+ payout_schedule = fields.Selection(string='Schedule', selection=[
+ ('day', 'Daily'),
+ ('week', 'Weekly'),
+ ('month', 'Monthly'),
+ ], default='week', required=True)
+ next_scheduled_payout = fields.Date('Next scheduled payout', compute=_compute_next_scheduled_payout, store=True)
+ transaction_ids = fields.One2many('adyen.transaction', 'adyen_payout_id', string='Transactions')
+
+ @api.model
+ def create(self, values):
+ adyen_payout_id = super(AdyenPayout, self).create(values)
+ if not adyen_payout_id.env.context.get('update_from_adyen'):
+ response = adyen_payout_id.adyen_account_id._adyen_rpc('create_payout', {
+ 'accountHolderCode': adyen_payout_id.adyen_account_id.account_holder_code,
+ })
+ adyen_payout_id.with_context(update_from_adyen=True).write({
+ 'code': response['accountCode'],
+ })
+ return adyen_payout_id
+
+ def unlink(self):
+ for adyen_payout_id in self:
+ adyen_payout_id.adyen_account_id._adyen_rpc('close_payout', {
+ 'accountCode': adyen_payout_id.code,
+ })
+ return super(AdyenPayout, self).unlink()
+
+ @api.model
+ def _process_payouts(self):
+ for adyen_payout_id in self.search([('next_scheduled_payout', '<', fields.Date.today())]):
+ adyen_payout_id.send_payout_request(notify=False)
+ adyen_payout_id._compute_next_scheduled_payout()
+
+ def send_payout_request(self, notify=True):
+ response = self.adyen_account_id._adyen_rpc('account_holder_balance', {
+ 'accountHolderCode': self.adyen_account_id.account_holder_code,
+ })
+ balances = next(account_balance['detailBalance']['balance'] for account_balance in response['balancePerAccount'] if account_balance['accountCode'] == self.code)
+ if notify and not balances:
+ self.env['bus.bus'].sendone(
+ (self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
+ {'type': 'simple_notification', 'title': _('No pending balance'), 'message': _('No balance is currently awaitng payout.')}
+ )
+ for balance in balances:
+ response = self.adyen_account_id._adyen_rpc('payout_request', {
+ 'accountCode': self.code,
+ 'accountHolderCode': self.adyen_account_id.account_holder_code,
+ 'bankAccountUUID': self.adyen_bank_account_id.bank_account_uuid or None,
+ 'amount': balance,
+ })
+ if notify and response['resultCode'] == 'Received':
+ currency_id = self.env['res.currency'].search([('name', '=', balance['currency'])])
+ value = round(balance['value'] / (10 ** currency_id.decimal_places), 2) # Convert from minor units
+ amount = str(value) + currency_id.symbol if currency_id.position == 'after' else currency_id.symbol + str(value)
+ message = _('Successfully sent payout request for %s', amount)
+ self.env['bus.bus'].sendone(
+ (self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
+ {'type': 'simple_notification', 'title': _('Payout Request sent'), 'message': message}
+ )
+
+ def _fetch_transactions(self, page=1):
+ response = self.adyen_account_id._adyen_rpc('get_transactions', {
+ 'accountHolderCode': self.adyen_account_id.account_holder_code,
+ 'transactionListsPerAccount': [{
+ 'accountCode': self.code,
+ 'page': page,
+ }]
+ })
+ transaction_list = response['accountTransactionLists'][0]
+ return transaction_list['transactions'], transaction_list['hasNextPage']
diff --git a/addons/adyen_platforms/models/adyen_transaction.py b/addons/adyen_platforms/models/adyen_transaction.py
new file mode 100644
index 00000000000..a04d8e33cd2
--- /dev/null
+++ b/addons/adyen_platforms/models/adyen_transaction.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime
+from pytz import UTC
+
+from odoo import api, fields, models
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
+
+
+class AdyenTransaction(models.Model):
+ _name = 'adyen.transaction'
+ _description = 'Adyen for Plaforms Transaction'
+ _order = 'date desc'
+
+ adyen_account_id = fields.Many2one('adyen.account')
+ reference = fields.Char('Reference')
+ amount = fields.Float('Amount')
+ currency_id = fields.Many2one('res.currency', string='Currency')
+ date = fields.Datetime('Date')
+ description = fields.Char('Description')
+ status = fields.Selection(string='Type', selection=[
+ ('PendingCredit', 'Pending Credit'),
+ ('CreditFailed', 'Credit Failed'),
+ ('Credited', 'Credited'),
+ ('Converted', 'Converted'),
+ ('PendingDebit', 'Pending Debit'),
+ ('DebitFailed', 'Debit Failed'),
+ ('Debited', 'Debited'),
+ ('DebitReversedReceived', 'Debit Reversed Received'),
+ ('DebitedReversed', 'Debit Reversed'),
+ ('ChargebackReceived', 'Chargeback Received'),
+ ('Chargeback', 'Chargeback'),
+ ('ChargebackReversedReceived', 'Chargeback Reversed Received'),
+ ('ChargebackReversed', 'Chargeback Reversed'),
+ ('Payout', 'Payout'),
+ ('PayoutReversed', 'Payout Reversed'),
+ ('FundTransfer', 'Fund Transfer'),
+ ('PendingFundTransfer', 'Pending Fund Transfer'),
+ ('ManualCorrected', 'Manual Corrected'),
+ ])
+ adyen_payout_id = fields.Many2one('adyen.payout')
+
+ @api.model
+ def sync_adyen_transactions(self):
+ ''' Method called by cron to sync transactions from Adyen.
+ Updates the status of pending transactions and create missing ones.
+ '''
+ for payout_id in self.env['adyen.payout'].search([]):
+ page = 1
+ has_next_page = True
+ new_transactions = True
+ pending_statuses = ['PendingCredit', 'PendingDebit', 'DebitReversedReceived', 'ChargebackReceived', 'ChargebackReversedReceived', 'PendingFundTransfer']
+ pending_transaction_ids = payout_id.transaction_ids.filtered(lambda tr: tr.status in pending_statuses)
+
+ while has_next_page and (new_transactions or pending_transaction_ids):
+ # Fetch next transaction page
+ transactions, has_next_page = payout_id._fetch_transactions(page)
+ for transaction in transactions:
+ transaction_reference = transaction.get('paymentPspReference') or transaction.get('pspReference')
+ transaction_id = payout_id.transaction_ids.filtered(lambda tr: tr.reference == transaction_reference)
+ if transaction_id:
+ new_transactions = False
+ if transaction_id in pending_transaction_ids:
+ # Update transaction status
+ transaction_id.sudo().write({
+ 'status': transaction['transactionStatus'],
+ })
+ pending_transaction_ids -= transaction_id
+ else:
+ currency_id = self.env['res.currency'].search([('name', '=', transaction['amount']['currency'])])
+ # New transaction
+ self.env['adyen.transaction'].sudo().create({
+ 'adyen_account_id': payout_id.adyen_account_id.id,
+ 'reference': transaction_reference,
+ 'amount': transaction['amount']['value'] / (10 ** currency_id.decimal_places),
+ 'currency_id': currency_id.id,
+ 'date': datetime.strptime(transaction['creationDate'], '%Y-%m-%dT%H:%M:%S%z').astimezone(UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ 'description': transaction.get('description'),
+ 'status': transaction['transactionStatus'],
+ 'adyen_payout_id': payout_id.id,
+ })
+ page += 1
diff --git a/addons/adyen_platforms/models/res_company.py b/addons/adyen_platforms/models/res_company.py
new file mode 100644
index 00000000000..c4767889f10
--- /dev/null
+++ b/addons/adyen_platforms/models/res_company.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ResCompany(models.Model):
+ _inherit = "res.company"
+
+ adyen_account_id = fields.Many2one('adyen.account', string='Adyen Account', readonly=True)
diff --git a/addons/adyen_platforms/security/ir.model.access.csv b/addons/adyen_platforms/security/ir.model.access.csv
new file mode 100644
index 00000000000..62ebdc56fb1
--- /dev/null
+++ b/addons/adyen_platforms/security/ir.model.access.csv
@@ -0,0 +1,6 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_adyen_account_group_erp_manager,adyen.account,model_adyen_account,base.group_erp_manager,1,1,1,1
+access_adyen_shareholder_group_erp_manager,adyen.shareholder,model_adyen_shareholder,base.group_erp_manager,1,1,1,1
+access_adyen_bank_account_group_erp_manager,adyen.bank.account,model_adyen_bank_account,base.group_erp_manager,1,1,1,1
+access_adyen_payout_group_erp_manager,adyen.payout,model_adyen_payout,base.group_erp_manager,1,1,1,1
+access_adyen_transaction_group_erp_manager,adyen.transaction,model_adyen_transaction,base.group_erp_manager,1,0,0,0
diff --git a/addons/adyen_platforms/static/src/js/adyen_account_fields.js b/addons/adyen_platforms/static/src/js/adyen_account_fields.js
new file mode 100644
index 00000000000..804f27eab4b
--- /dev/null
+++ b/addons/adyen_platforms/static/src/js/adyen_account_fields.js
@@ -0,0 +1,20 @@
+odoo.define('adyen_platforms.fields', function (require) {
+"use strict";
+
+var core = require('web.core');
+var FieldSelection = require('web.relational_fields').FieldSelection;
+var field_registry = require('web.field_registry');
+
+var qweb = core.qweb;
+
+var AdyenKYCStatusTag = FieldSelection.extend({
+ _render: function () {
+ this.$el.append(qweb.render('AdyenKYCStatusTag', {
+ value: this.value,
+ }));
+ },
+});
+
+field_registry.add("adyen_kyc_status_tag", AdyenKYCStatusTag);
+
+});
diff --git a/addons/adyen_platforms/static/src/js/adyen_account_views.js b/addons/adyen_platforms/static/src/js/adyen_account_views.js
new file mode 100644
index 00000000000..8921a993590
--- /dev/null
+++ b/addons/adyen_platforms/static/src/js/adyen_account_views.js
@@ -0,0 +1,63 @@
+odoo.define('adyen_platforms.account_views', function (require) {
+"use strict";
+
+var core = require('web.core');
+var Dialog = require('web.Dialog');
+var FormController = require('web.FormController');
+var FormView = require('web.FormView');
+var viewRegistry = require('web.view_registry');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+var AdyenAccountFormController = FormController.extend({
+ _saveRecord: function (recordID, options) {
+ if(this.model.isNew(this.handle) && this.canBeSaved()) {
+ var _super = this._super.bind(this, recordID, options);
+ var buttons = [
+ {
+ text: _t("Create"),
+ classes: 'btn-primary o_adyen_confirm',
+ close: true,
+ disabled: true,
+ click: function () {
+ this.close();
+ _super();
+ },
+ },
+ {
+ text: _t("Cancel"),
+ close: true,
+ }
+ ];
+
+ var dialog = new Dialog(this, {
+ size: 'extra-large',
+ buttons: buttons,
+ title: _t("Confirm your Adyen Account Creation"),
+ $content: QWeb.render('AdyenAccountCreationConfirmation', {
+ data: this.model.get(this.handle).data,
+ }),
+ });
+
+ dialog.open().opened(function () {
+ dialog.$el.on('change', '.opt_in_checkbox', function (ev) {
+ ev.preventDefault();
+ dialog.$footer.find('.o_adyen_confirm')[0].disabled = !ev.currentTarget.checked;
+ });
+ });
+ } else if (!this.model.isNew(this.handle)) {
+ return this._super.apply(this, arguments);
+ }
+ },
+});
+
+var AdyenAccountFormView = FormView.extend({
+ config: _.extend({}, FormView.prototype.config, {
+ Controller: AdyenAccountFormController,
+ }),
+});
+
+viewRegistry.add('adyen_account_form', AdyenAccountFormView);
+
+});
diff --git a/addons/adyen_platforms/static/src/js/adyen_transactions.js b/addons/adyen_platforms/static/src/js/adyen_transactions.js
new file mode 100644
index 00000000000..3a6fe89631d
--- /dev/null
+++ b/addons/adyen_platforms/static/src/js/adyen_transactions.js
@@ -0,0 +1,33 @@
+odoo.define('adyen_platforms.transactions', function (require) {
+"use strict";
+
+var ListController = require('web.ListController');
+var ListView = require('web.ListView');
+var viewRegistry = require('web.view_registry');
+
+var TransactionsListController = ListController.extend({
+ buttons_template: 'AdyenTransactionsListView.buttons',
+ events: _.extend({}, ListController.prototype.events, {
+ 'click .o_button_sync_transactions': '_onTransactionsSync',
+ }),
+
+ _onTransactionsSync: function () {
+ var self = this;
+ this._rpc({
+ model: 'adyen.transaction',
+ method: 'sync_adyen_transactions',
+ args: [],
+ }).then(function () {
+ self.trigger_up('reload');
+ });
+ }
+});
+
+var TransactionsListView = ListView.extend({
+ config: _.extend({}, ListView.prototype.config, {
+ Controller: TransactionsListController,
+ }),
+});
+
+viewRegistry.add('adyen_transactions_tree', TransactionsListView);
+});
diff --git a/addons/adyen_platforms/static/src/xml/adyen_account_templates.xml b/addons/adyen_platforms/static/src/xml/adyen_account_templates.xml
new file mode 100644
index 00000000000..4bec48cdaa3
--- /dev/null
+++ b/addons/adyen_platforms/static/src/xml/adyen_account_templates.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates>
+ <t t-name="AdyenAccountCreationConfirmation">
+ <main>
+ <div class="alert alert-info">
+ You can start processing payments as soon as your application is sent.<br/>
+ Payouts will be blocked until your application has been accepted.<br/>
+ We will notify you when the status of the review changes or if additionnal data is required.<br/>
+ </div>
+
+ <h3>Submitted data</h3>
+ <table class="table table-sm">
+ <t t-if="data.is_business">
+ <tr>
+ <td>Legal Entity</td>
+ <td>Business</td>
+ </tr>
+ <tr>
+ <td>Legal Business Name</td>
+ <td t-esc="data.legal_business_name"/>
+ </tr>
+ <tr>
+ <td>Doing Business As</td>
+ <td t-esc="data.doing_business_as"/>
+ </tr>
+ <tr>
+ <td>Registration Number</td>
+ <td t-esc="data.registration_number"/>
+ </tr>
+ </t>
+ <t t-else="">
+ <tr>
+ <td>Legal Entity</td>
+ <td>Individual</td>
+ </tr>
+ <tr>
+ <td>First Name</td>
+ <td t-esc="data.first_name"/>
+ </tr>
+ <tr>
+ <td>Last Name</td>
+ <td t-esc="data.last_name"/>
+ </tr>
+ <tr>
+ <td>Date of Birth</td>
+ <td t-esc="data.date_of_birth.format('YYYY-MM-DD')"/>
+ </tr>
+ </t>
+ <tr>
+ <td>Email</td>
+ <td t-esc="data.email"/>
+ </tr>
+ <tr>
+ <td>Phone Number</td>
+ <td t-esc="data.phone_number"/>
+ </tr>
+ <tr>
+ <td>Address</td>
+ <td>
+ <t t-esc="data.street"/> <t t-esc="data.house_number_or_name"/>,
+ <t t-esc="data.city"/> <t t-if="data.state_code" t-esc="data.state_code"/> <t t-esc="data.zip"/>,
+ <t t-esc="data.country_code"/>
+ </td>
+ </tr>
+ </table>
+
+ <h3>Disclaimers</h3>
+ <p>WARNING: Please note that the right to use the payment services is only for
+ sales in your own name. You may not resell, hire or on any other basis allow third
+ parties to use the payment services to enable such third parties to be paid for their
+ services. You may not use the payment services for different types of product and
+ services than as registered with your application. In particular you confirm that you
+ will not use the payment services for any type of product or service appearing in the
+ <a href="https://www.odoo.com/odoo_adyen/static/src/pdf/marketpay_prohibited_products_and_services.pdf">
+ Prohibited Products and Services List of our Processor</a>. If we or our
+ Processor at any time discover that the information you provided about your
+ business is incorrect or has changed without informing us or if you violate any of
+ these conditions, the services may be suspended and/or terminated with
+ immediate effect and fines may be applied by the Credit Card Schemes and/or the
+ authorities for unregistered or inappropriate use of payment services which will in
+ such case be payable by you.</p>
+
+ <h3>Contractual Relationship</h3>
+ <p>The payment processing services ordered by you by placing this order will be
+ provided to you by Adyen N.V. (hereafter “Processor”), with which you are
+ entering into a direct agreement by confirming this order. Odoo S.A.
+ (hereafter “We /Us”) will assist and support you in your use of the services to be
+ provided by the Processor and we will provide you first line assistance with and
+ enable you to connect to the systems of Processor to be able to use its services.
+ For this purpose, you hereby instruct Processor to provide us access to your data
+ and setting in Processor’s systems which are used by Processor to provide the
+ services and authorise us to manage these on your behalf.</p>
+
+ <h3>Pricing</h3>
+ <ul>
+ <li>Payment and processing fees are listed on <a href="http://www.adyen.com/pricing">adyen.com/pricing</a>.</li>
+ <li>Payouts cost 0.20€ (EU) or 0.22$ (US) each.</li>
+ <li>Chargebacks cost 7.5€ each.</li>
+ <li>Onboarding and KYC cost 5€.</li>
+ </ul>
+
+ <input type="checkbox" class="opt_in_checkbox"/>
+ I confirm I have taken notice of and accept the following terms and restrictions:
+ <ul>
+ <li>Adyen MarketPay Terms and Conditions (click <a href="https://www.odoo.com/odoo_adyen/static/src/pdf/marketpay_terms_and_conditions.pdf">here</a> to download and review)</li>
+ <li>Adyen Restricted and Prohibited Products and Services list (click <a href="https://www.odoo.com/odoo_adyen/static/src/pdf/marketpay_prohibited_products_and_services.pdf">here</a> to download and review)</li>
+ </ul>
+ </main>
+ </t>
+
+ <t t-name="AdyenKYCStatusTag">
+ <t t-if="value === 'awaiting_data'">
+ <span class="badge badge-pill badge-warning">Data to provide</span>
+ </t>
+ <t t-if="value === 'pending'">
+ <span class="badge badge-pill badge-info">Waiting for validation</span>
+ </t>
+ <t t-if="value === 'passed'">
+ <span class="badge badge-pill badge-success">Confirmed</span>
+ </t>
+ <t t-if="value === 'failed'">
+ <span class="badge badge-pill badge-danger">Failed</span>
+ </t>
+ </t>
+</templates>
diff --git a/addons/adyen_platforms/static/src/xml/adyen_transactions_templates.xml b/addons/adyen_platforms/static/src/xml/adyen_transactions_templates.xml
new file mode 100644
index 00000000000..50552e80c6f
--- /dev/null
+++ b/addons/adyen_platforms/static/src/xml/adyen_transactions_templates.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates>
+ <t t-extend="ListView.buttons" t-name="AdyenTransactionsListView.buttons">
+ <t t-jquery="button.o_list_button_discard" t-operation="after">
+ <button type="button" class="btn btn-primary o_button_sync_transactions">
+ Sync Transactions
+ </button>
+ </t>
+ </t>
+</templates>
diff --git a/addons/adyen_platforms/util.py b/addons/adyen_platforms/util.py
new file mode 100644
index 00000000000..5352a25192b
--- /dev/null
+++ b/addons/adyen_platforms/util.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+import hashlib
+import hmac
+import json
+import requests
+import time
+import werkzeug.urls
+
+class AdyenProxyAuth(requests.auth.AuthBase):
+ def __init__(self, adyen_account_id):
+ super(AdyenProxyAuth, self).__init__()
+ self.adyen_account_id = adyen_account_id
+
+ def __call__(self, request):
+ h = hmac.new(self.adyen_account_id.proxy_token.encode('utf-8'), digestmod=hashlib.sha256)
+
+ # Craft the message (timestamp|url path|query params|body content)
+ msg_timestamp = int(time.time())
+ parsed_url = werkzeug.urls.url_parse(request.path_url)
+ body = request.body
+ if isinstance(body, bytes):
+ body = body.decode('utf-8')
+ body = json.loads(body)
+
+ message = '%s|%s|%s|%s' % (
+ msg_timestamp, # timestamp
+ parsed_url.path, # url path
+ json.dumps(werkzeug.urls.url_decode(parsed_url.query), sort_keys=True), # url query params sorted by key
+ json.dumps(body, sort_keys=True)) # request body
+
+ h.update(message.encode('utf-8')) # digest the message
+
+ request.headers.update({
+ 'oe-adyen-uuid': self.adyen_account_id.adyen_uuid,
+ 'oe-signature': base64.b64encode(h.digest()),
+ 'oe-timestamp': msg_timestamp,
+ })
+
+ return request
diff --git a/addons/adyen_platforms/views/adyen_account_templates.xml b/addons/adyen_platforms/views/adyen_account_templates.xml
new file mode 100644
index 00000000000..a16df255051
--- /dev/null
+++ b/addons/adyen_platforms/views/adyen_account_templates.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="kyc_status_message">
+ <p>New KYC Status: <t- t-esc="kyc_status"/></p>
+ <p t-if="account_holder_messages + shareholder_messages + bank_account_messages">
+ Reason(s):
+ <ul>
+ <li t-if="account_holder_messages" t-foreach="account_holder_messages" t-as="message">
+ <t t-esc="message"/>
+ </li>
+ <li t-if="shareholder_messages">
+ Shareholders:
+ <ul>
+ <li t-foreach="shareholder_messages" t-as="message">
+ <t t-esc="message"/>
+ </li>
+ </ul>
+ </li>
+ <li t-if="bank_account_messages">
+ Bank Accounts:
+ <ul>
+ <li t-foreach="bank_account_messages" t-as="message">
+ <t t-esc="message"/>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </p>
+ </template>
+</odoo>
diff --git a/addons/adyen_platforms/views/adyen_account_views.xml b/addons/adyen_platforms/views/adyen_account_views.xml
new file mode 100644
index 00000000000..0eb10fb6752
--- /dev/null
+++ b/addons/adyen_platforms/views/adyen_account_views.xml
@@ -0,0 +1,243 @@
+<?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</field>
+ <field name="model">adyen.account</field>
+ <field name="arch" type="xml">
+ <form string="Adyen Account" create="false" js_class="adyen_account_form">
+ <header>
+ <field name="kyc_status" widget="statusbar" statusbar_visible="awaiting_data,pending,passed"/>
+ </header>
+ <sheet>
+ <div class="oe_button_box" name="button_box">
+ <button name="action_show_transactions" type="object"
+ class="oe_stat_button" icon="fa-credit-card">
+ <div class="o_stat_info">
+ <field name="transactions_count" class="o_stat_value"/>
+ <span class="o_stat_text"> Transactions</span>
+ </div>
+ </button>
+ </div>
+
+ <group>
+ <field name="adyen_uuid" invisible="1"/>
+
+ <group>
+ <field name="is_business" attrs="{'readonly': [('id', '!=', False)]}"/>
+ </group>
+
+ <group>
+ <field name="company_id" readonly="1"/>
+ </group>
+
+ <group string="Individual" attrs="{'invisible': [('is_business', '=', True)]}">
+ <field name="first_name"
+ attrs="{'required': [('is_business', '=', False)]}"/>
+ <field name="last_name"
+ attrs="{'required': [('is_business', '=', False)]}"/>
+ <field name="date_of_birth"
+ attrs="{'required': [('is_business', '=', False)]}"/>
+ <field name="document_number"
+ attrs="{'invisible': [('country_code', 'not in', ['AU', 'CA', 'GR', 'IT', 'US'])], 'required': [('is_business', '=', False), ('country_id', 'in', ['AU', 'CA', 'GR', 'IT', 'US'])]}"/>
+ <field name="document_type"
+ attrs="{'invisible': [('country_code', '!=', 'AU')], 'required': [('is_business', '=', False), ('country_id', '=', 'AU')]}"/>
+ <field name="id_type" attrs="{'invisible': [('adyen_uuid', '=', False)]}"/>
+ <field name="id_front" filename="id_front_filename" attrs="{'invisible': [('adyen_uuid', '=', False)]}"/>
+ <field name="id_front_filename" invisible="1"/>
+ <field name="id_back" filename="id_back_filename"
+ attrs="{'invisible': [('id_type', 'not in', ['ID_CARD', 'DRIVING_LICENSE'])]}"/>
+ <field name="id_back_filename" invisible="1"/>
+ </group>
+
+ <group string="Business" attrs="{'invisible': [('is_business', '=', False)]}">
+ <field name="legal_business_name"
+ attrs="{'required': [('is_business', '=', True)]}"/>
+ <field name="doing_business_as"
+ attrs="{'required': [('is_business', '=', True)]}"/>
+ <field name="registration_number"
+ attrs="{'required': [('is_business', '=', True)]}"/>
+ </group>
+
+ <group string="Contact">
+ <field name="email" widget="email"/>
+ <field name="phone_number"/>
+ <label for="street" string="Address"/>
+ <div class="o_address_format">
+ <field name="street" placeholder="Street" class="o_address_street"/>
+ <field name="house_number_or_name" placeholder="House number or name" class="o_address_street"/>
+ <field name="city" placeholder="City" class="o_address_city"/>
+ <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'])]}"/>
+ <field name="state_code" invisible="1"/>
+ <field name="zip" placeholder="ZIP" class="o_address_zip"/>
+ <field name="country_id" placeholder="Country" class="o_address_country" options="{'no_open': True, 'no_create': True}"/>
+ <field name="country_code" invisible="1"/>
+ </div>
+ </group>
+ </group>
+ <notebook attrs="{'invisible': [('adyen_uuid', '=', False)]}">
+ <page string="Shareholders" attrs="{'invisible': [('is_business', '=', False)], 'required': [('is_business', '=', True)]}">
+ <field name="shareholder_ids">
+ <tree>
+ <field name="first_name"/>
+ <field name="last_name"/>
+ <field name="kyc_status" widget="adyen_kyc_status_tag"/>
+ </tree>
+ </field>
+ </page>
+ <page string="Bank Accounts">
+ <field name="bank_account_ids">
+ <tree>
+ <field name="owner_name"/>
+ <field name="iban"/>
+ <field name="account_number"/>
+ <field name="kyc_status" widget="adyen_kyc_status_tag"/>
+ </tree>
+ </field>
+ </page>
+ <page string="Payouts">
+ <field name="payout_ids">
+ <tree>
+ <field name="name"/>
+ <field name="payout_schedule"/>
+ </tree>
+ </field>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="adyen_shareholder_view_form" model="ir.ui.view">
+ <field name="name">adyen.shareholder.view.form</field>
+ <field name="model">adyen.shareholder</field>
+ <field name="arch" type="xml">
+ <form string="Adyen Shareholder">
+ <header>
+ <field name="kyc_status" widget="statusbar" statusbar_visible="awaiting_data,pending,passed"/>
+ </header>
+ <sheet>
+ <div class="alert alert-warning" role="alert"
+ attrs="{'invisible': [('kyc_status_message', '=', False)]}">
+ <field name="kyc_status_message"/>
+ </div>
+ <group>
+ <field name="shareholder_uuid" invisible="1"/>
+ <group>
+ <field name="first_name"/>
+ <field name="last_name"/>
+ <field name="date_of_birth"/>
+ </group>
+ <group>
+ <field name="document_number"
+ attrs="{'invisible': [('country_code', 'not in', ['IT', 'US'])], 'required': [('country_code', 'in', ['IT', 'US'])]}"/>
+ <field name="id_type" attrs="{'invisible': [('shareholder_uuid', '=', False)]}"/>
+ <field name="id_front" filename="id_front_filename" attrs="{'invisible': [('shareholder_uuid', '=', False)]}"/>
+ <field name="id_front_filename" invisible="1"/>
+ <field name="id_back" filename="id_back_filename"
+ attrs="{'required': [('id_type', 'in', ['ID_CARD', 'DRIVING_LICENSE'])], 'invisible': [('id_type', 'not in', ['ID_CARD', 'DRIVING_LICENSE'])]}"/>
+ <field name="id_back_filename" invisible="1"/>
+ </group>
+ <group>
+ <label for="street" string="Address"/>
+ <div class="o_address_format">
+ <field name="street" placeholder="Street" class="o_address_street"/>
+ <field name="house_number_or_name" placeholder="House number or name" class="o_address_street"/>
+ <field name="city" placeholder="City" class="o_address_city"/>
+ <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'])]}"/>
+ <field name="zip" placeholder="ZIP" class="o_address_zip"/>
+ <field name="country_id" placeholder="Country" class="o_address_country" options="{'no_open': True, 'no_create': True}"/>
+ <field name="country_code" invisible="1"/>
+ </div>
+ </group>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="adyen_bank_account_view_form" model="ir.ui.view">
+ <field name="name">adyen.bank.account.view.form</field>
+ <field name="model">adyen.bank.account</field>
+ <field name="arch" type="xml">
+ <form string="Adyen Bank Account">
+ <header>
+ <field name="kyc_status" widget="statusbar" statusbar_visible="awaiting_data,pending,passed"/>
+ </header>
+ <sheet>
+ <div class="alert alert-warning" role="alert"
+ attrs="{'invisible': [('kyc_status_message', '=', False)]}">
+ <field name="kyc_status_message"/>
+ </div>
+ <group>
+ <field name="bank_account_uuid" invisible="1"/>
+ <group>
+ <field name="country_id" options='{"no_open": True, "no_create": True}'/>
+ <field name="country_code" invisible="1"/>
+ <field name="currency_id"/>
+ <field name="iban"
+ attrs="{'invisible': [('country_code', 'not in', ['AT', 'BE', 'CH', 'CZ', 'DE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'IE', 'IT', 'LT', 'LU', 'NL', 'PL', 'PT'])],
+ 'required': [('country_code', 'in', ['AT', 'BE', 'CH', 'CZ', 'DE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'IE', 'IT', 'LT', 'LU', 'NL', 'PL', 'PT'])]}"/>
+ <field name="account_type"
+ attrs="{'invisible': [('country_code', '!=', 'US')], 'required': [('country_code', '=', 'US')]}"/>
+ <field name="account_number"
+ attrs="{'invisible': [('country_code', 'not in', ['AU', 'CA', 'US'])], 'required': [('country_code', 'in', ['AU', 'CA', 'US'])]}"/>
+ <field name="branch_code"
+ attrs="{'invisible': [('country_code', 'not in', ['AU', 'CA', 'US'])], 'required': [('country_code', 'in', ['AU', 'CA', 'US'])]}"/>
+ <field name="bank_code"
+ attrs="{'invisible': [('country_code', '!=', 'CA')], 'required': [('country_code', '=', 'CA')]}"/>
+ <field name="bank_statement" filename="bank_statement_filename" attrs="{'invisible': [('bank_account_uuid', '=', False)]}"/>
+ <field name="bank_statement_filename" invisible="1"/>
+ </group>
+ <group>
+ <field name="owner_name"/>
+ <label for="owner_street" string="Owner Address" attrs="{'invisible': [('country_code', 'not in', ['CA', 'US'])]}"/>
+ <div class="o_address_format" attrs="{'invisible': [('country_code', 'not in', ['CA', 'US'])]}">
+ <field name="owner_street" placeholder="Street" class="o_address_street" attrs="{'required': [('country_code', 'in', ['CA', 'US'])]}"/>
+ <field name="owner_house_number_or_name" placeholder="House number or name" class="o_address_street" attrs="{'required': [('country_code', 'in', ['CA', 'US'])]}"/>
+ <field name="owner_city" placeholder="City" class="o_address_city" attrs="{'required': [('country_code', 'in', ['CA', 'US'])]}"/>
+ <field name="owner_state_id" class="o_address_state" placeholder="State" options="{'no_open': True, 'no_quick_create': True}"
+ attrs="{'required': [('country_code', 'in', ['CA', 'US'])]}"/>
+ <field name="owner_zip" placeholder="ZIP" class="o_address_zip" attrs="{'required': [('country_code', 'in', ['CA', 'US'])]}"/>
+ <field name="owner_country_id" placeholder="Country" class="o_address_country" options='{"no_open": True, "no_create": True}'
+ attrs="{'required': [('country_code', 'in', ['CA', 'US'])]}"/>
+ </div>
+ </group>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="adyen_payout_view_form" model="ir.ui.view">
+ <field name="name">adyen.payout.view.form</field>
+ <field name="model">adyen.payout</field>
+ <field name="arch" type="xml">
+ <form string="Adyen Payout">
+ <header>
+ <button name="send_payout_request" string="Request a payout now" class="oe_highlight" type="object" attrs="{'invisible': [('code', '=', False)]}"/>
+ </header>
+ <sheet>
+ <group>
+ <field name="code" invisible="1"/>
+ <field name="name"/>
+ <field name="payout_schedule"/>
+ <field name="adyen_bank_account_id"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="adyen_account_action_create" model="ir.actions.act_window">
+ <field name="name">Create an Adyen Account</field>
+ <field name="res_model">adyen.account</field>
+ <field name="view_mode">form</field>
+ </record>
+</odoo>
diff --git a/addons/adyen_platforms/views/adyen_transaction_views.xml b/addons/adyen_platforms/views/adyen_transaction_views.xml
new file mode 100644
index 00000000000..667a4d3688e
--- /dev/null
+++ b/addons/adyen_platforms/views/adyen_transaction_views.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="adyen_transaction_view_form" model="ir.ui.view">
+ <field name="name">adyen.transaction.view.form</field>
+ <field name="model">adyen.transaction</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet>
+ <group>
+ <field name="reference"/>
+ <field name="amount" widget='monetary'/>
+ <field name="currency_id" invisible="1"/>
+ <field name="date"/>
+ <field name="description"/>
+ <field name="status"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="adyen_transaction_view_list" model="ir.ui.view">
+ <field name="name">adyen.transaction.view.list</field>
+ <field name="model">adyen.transaction</field>
+ <field name="arch" type="xml">
+ <tree js_class="adyen_transactions_tree">
+ <field name="reference"/>
+ <field name="amount" widget='monetary'/>
+ <field name="currency_id" invisible="1"/>
+ <field name="date"/>
+ <field name="description"/>
+ <field name="status"/>
+ </tree>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/adyen_platforms/views/assets.xml b/addons/adyen_platforms/views/assets.xml
new file mode 100644
index 00000000000..e1d29146b66
--- /dev/null
+++ b/addons/adyen_platforms/views/assets.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="assets_backend" inherit_id="web.assets_backend" name="Adyen for Platforms Backend Assets">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/adyen_platforms/static/src/js/adyen_account_fields.js"></script>
+ <script type="text/javascript" src="/adyen_platforms/static/src/js/adyen_account_views.js"></script>
+ <script type="text/javascript" src="/adyen_platforms/static/src/js/adyen_transactions.js"></script>
+ </xpath>
+ </template>
+</odoo>
diff --git a/addons/point_of_sale/views/res_config_settings_views.xml b/addons/point_of_sale/views/res_config_settings_views.xml
index 2e43ee4a10f..e9e49c0a538 100644
--- a/addons/point_of_sale/views/res_config_settings_views.xml
+++ b/addons/point_of_sale/views/res_config_settings_views.xml
@@ -91,6 +91,19 @@
<h2>Payment Terminals</h2>
<div class="row mt16 o_settings_container" id="o_settings_use_payment_terminals">
<div class="col-12 col-lg-6 o_setting_box"
+ id="adyen_payment_terminal_setting"
+ title="The transactions are processed by Adyen. Set your Adyen credentials on the related payment method.">
+ <div class="o_setting_left_pane">
+ <field name="module_pos_adyen"/>
+ </div>
+ <div class="o_setting_right_pane">
+ <label for="module_pos_adyen" string="Adyen"/>
+ <div class="text-muted">
+ Accept payments with an Adyen payment terminal
+ </div>
+ </div>
+ </div>
+ <div class="col-12 col-lg-6 o_setting_box"
id="vantiv_payment_terminal_setting"
title="The transactions are processed by Vantiv. Set your Vantiv credentials on the related payment method.">
<div class="o_setting_left_pane">
@@ -108,19 +121,6 @@
</div>
</div>
</div>
- <div class="col-12 col-lg-6 o_setting_box"
- id="adyen_payment_terminal_setting"
- title="The transactions are processed by Adyen. Set your Adyen credentials on the related payment method.">
- <div class="o_setting_left_pane">
- <field name="module_pos_adyen"/>
- </div>
- <div class="o_setting_right_pane">
- <label for="module_pos_adyen" string="Adyen"/>
- <div class="text-muted">
- Accept payments with an Adyen payment terminal
- </div>
- </div>
- </div>
<div class="col-12 col-lg-6 o_setting_box" title="The transactions are processed by Six. Set the IP address of the terminal on the related payment method.">
<div class="o_setting_left_pane">
<field name="module_pos_six"/>
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>
diff --git a/addons/pos_restaurant_adyen/models/pos_payment.py b/addons/pos_restaurant_adyen/models/pos_payment.py
index 5f38d0159f3..4c62cacf50c 100644
--- a/addons/pos_restaurant_adyen/models/pos_payment.py
+++ b/addons/pos_restaurant_adyen/models/pos_payment.py
@@ -29,10 +29,4 @@ class PosPayment(models.Model):
'merchantAccount': self.payment_method_id.adyen_merchant_account,
}
- return self.payment_method_id.proxy_adyen_request(
- data,
- self.payment_method_id.adyen_test_mode,
- self.payment_method_id.adyen_api_key,
- 'https://pal-test.adyen.com/pal/servlet/Payment/v52/capture',
- 'https://pal-live.adyen.com/pal/servlet/Payment/v52/capture',
- )
+ return self.payment_method_id.proxy_adyen_request(data, 'capture')
diff --git a/addons/pos_restaurant_adyen/models/pos_payment_method.py b/addons/pos_restaurant_adyen/models/pos_payment_method.py
index 9f928363379..e40b6e08f3e 100644
--- a/addons/pos_restaurant_adyen/models/pos_payment_method.py
+++ b/addons/pos_restaurant_adyen/models/pos_payment_method.py
@@ -8,3 +8,10 @@ class PosPaymentMethod(models.Model):
_inherit = 'pos.payment.method'
adyen_merchant_account = fields.Char(help='The POS merchant account code used in Adyen')
+
+ def _get_adyen_endpoints(self):
+ return {
+ **super(PosPaymentMethod, self).get_adyen_endpoints(),
+ 'adjust': 'https://pal-%s.adyen.com/pal/servlet/Payment/v52/adjustAuthorisation',
+ 'capture': 'https://pal-%s.adyen.com/pal/servlet/Payment/v52/capture',
+ }
diff --git a/addons/pos_restaurant_adyen/static/src/js/payment_adyen.js b/addons/pos_restaurant_adyen/static/src/js/payment_adyen.js
index d94e990fdf3..6bdb845c92b 100644
--- a/addons/pos_restaurant_adyen/static/src/js/payment_adyen.js
+++ b/addons/pos_restaurant_adyen/static/src/js/payment_adyen.js
@@ -32,11 +32,7 @@ odoo.define('pos_restaurant_adyen.payment', function (require) {
},
};
- return this._call_adyen(
- data,
- 'https://pal-test.adyen.com/pal/servlet/Payment/v52/adjustAuthorisation',
- 'https://pal-live.adyen.com/pal/servlet/Payment/v52/adjustAuthorisation',
- );
+ return this._call_adyen(data, 'adjust');
},
canBeAdjusted: function (cid) {