diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/pos_adyen/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/pos_adyen/models')
| -rw-r--r-- | addons/pos_adyen/models/__init__.py | 7 | ||||
| -rw-r--r-- | addons/pos_adyen/models/adyen_account.py | 111 | ||||
| -rw-r--r-- | addons/pos_adyen/models/pos_config.py | 25 | ||||
| -rw-r--r-- | addons/pos_adyen/models/pos_payment_method.py | 148 | ||||
| -rw-r--r-- | addons/pos_adyen/models/res_config_settings.py | 13 |
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() |
