summaryrefslogtreecommitdiff
path: root/addons/pos_adyen/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/pos_adyen/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/pos_adyen/models')
-rw-r--r--addons/pos_adyen/models/__init__.py7
-rw-r--r--addons/pos_adyen/models/adyen_account.py111
-rw-r--r--addons/pos_adyen/models/pos_config.py25
-rw-r--r--addons/pos_adyen/models/pos_payment_method.py148
-rw-r--r--addons/pos_adyen/models/res_config_settings.py13
5 files changed, 304 insertions, 0 deletions
diff --git a/addons/pos_adyen/models/__init__.py b/addons/pos_adyen/models/__init__.py
new file mode 100644
index 00000000..d8557bb1
--- /dev/null
+++ b/addons/pos_adyen/models/__init__.py
@@ -0,0 +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 00000000..e6621b4a
--- /dev/null
+++ b/addons/pos_adyen/models/adyen_account.py
@@ -0,0 +1,111 @@
+# -*- 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')
+
+ @api.model
+ def _sync_adyen_cron(self):
+ self.env['adyen.terminal']._sync_adyen_terminals()
+ super(AdyenAccount, self)._sync_adyen_cron()
+
+ 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 Platforms 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('create_store', 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,
+ '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 Platforms 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_config.py b/addons/pos_adyen/models/pos_config.py
new file mode 100644
index 00000000..9e388279
--- /dev/null
+++ b/addons/pos_adyen/models/pos_config.py
@@ -0,0 +1,25 @@
+# coding: utf-8
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
+
+from odoo import api, fields, models, _
+from odoo.exceptions import ValidationError
+
+_logger = logging.getLogger(__name__)
+
+
+class PosConfig(models.Model):
+ _inherit = 'pos.config'
+
+ adyen_ask_customer_for_tip = fields.Boolean('Ask Customers For Tip', help='Prompt the customer to tip.')
+
+ @api.onchange('iface_tipproduct')
+ def _onchange_iface_tipproduct_adyen(self):
+ if not self.iface_tipproduct:
+ self.adyen_ask_customer_for_tip = False
+
+ @api.constrains('adyen_ask_customer_for_tip', 'iface_tipproduct', 'tip_product_id')
+ def _check_adyen_ask_customer_for_tip(self):
+ for config in self:
+ if config.adyen_ask_customer_for_tip and (not config.tip_product_id or not config.iface_tipproduct):
+ raise ValidationError(_("Please configure a tip product for POS %s to support tipping with Adyen.", config.name))
diff --git a/addons/pos_adyen/models/pos_payment_method.py b/addons/pos_adyen/models/pos_payment_method.py
new file mode 100644
index 00000000..73c09287
--- /dev/null
+++ b/addons/pos_adyen/models/pos_payment_method.py
@@ -0,0 +1,148 @@
+# coding: utf-8
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import json
+import logging
+import pprint
+import random
+import requests
+import string
+from werkzeug.exceptions import Forbidden
+
+from odoo import fields, models, api, _
+from odoo.exceptions import ValidationError
+
+_logger = logging.getLogger(__name__)
+
+class PosPaymentMethod(models.Model):
+ _inherit = 'pos.payment.method'
+
+ def _get_payment_terminal_selection(self):
+ 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')
+
+ @api.constrains('adyen_terminal_identifier')
+ def _check_adyen_terminal_identifier(self):
+ for payment_method in self:
+ if not payment_method.adyen_terminal_identifier:
+ continue
+ existing_payment_method = self.search([('id', '!=', payment_method.id),
+ ('adyen_terminal_identifier', '=', payment_method.adyen_terminal_identifier)],
+ limit=1)
+ if existing_payment_method:
+ 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)
+
+ def _adyen_diagnosis_request_data(self, pos_config_name):
+ service_id = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
+ return {
+ "SaleToPOIRequest": {
+ "MessageHeader": {
+ "ProtocolVersion": "3.0",
+ "MessageClass": "Service",
+ "MessageCategory": "Diagnosis",
+ "MessageType": "Request",
+ "ServiceID": service_id,
+ "SaleID": pos_config_name,
+ "POIID": self.adyen_terminal_identifier,
+ },
+ "DiagnosisRequest": {
+ "HostDiagnosisFlag": False
+ }
+ }
+ }
+
+ def get_latest_adyen_status(self, pos_config_name):
+ self.ensure_one()
+
+ # Poll the status of the terminal if there's no new
+ # notification we received. This is done so we can quickly
+ # notify the user if the terminal is no longer reachable due
+ # to connectivity issues.
+ self.proxy_adyen_request(self._adyen_diagnosis_request_data(pos_config_name))
+
+ latest_response = self.sudo().adyen_latest_response
+ latest_response = json.loads(latest_response) if latest_response else False
+ self.sudo().adyen_latest_response = '' # avoid handling old responses multiple times
+
+ return {
+ 'latest_response': latest_response,
+ 'last_received_diagnosis_id': self.sudo().adyen_latest_diagnosis,
+ }
+
+ def proxy_adyen_request(self, data, operation=False):
+ ''' Necessary because Adyen's endpoints don't have CORS enabled '''
+ if not operation:
+ operation = 'terminal_request'
+
+ 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)
+
+ 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,
+ }
+ req = requests.post(endpoint, json=data, headers=headers, timeout=TIMEOUT)
+
+ # Authentication error doesn't return JSON
+ if req.status_code == 401:
+ return {
+ 'error': {
+ 'status_code': req.status_code,
+ 'message': req.text
+ }
+ }
+
+ if req.text == 'ok':
+ return True
+
+ return req.json()
+
+ 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 00000000..4e3b7102
--- /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()