[REM] adyen_platforms, *: remove Odoo Payments modules

*: extensions of `adyen_platforms`, namely:
   - pos_adyen: remove related code, records, views, ...
   - payment_odoo: remove the module
   - sale_payment_odoo: remove the module

Modules related to Odoo Payments are already all merged since 14.0 and
14.4 (depending on the module). As the plan is to merge the final
version of Odoo Payments in 15.0 soon after the release, we want to
avoid all the complications that inevitably come with huge diffs, model
changes, XMLID collisions, etc. when merging in a stable version. Even
though it is possible to install these modules since 14.0, no
information could be lost during the upgrade since the entry point of
the application has never been activated.

task-2637770

closes odoo/odoo#75852

Related: odoo/upgrade#2804
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Co-authored-by: Antoine Vandevenne (anv)
This commit is contained in:
Victor Feyens
2021-09-06 14:35:58 +00:00
committed by Antoine Vandevenne (anv)
co-authored by Antoine Vandevenne (anv)
parent 7cc67aecec
commit fbfd99e680
68 changed files with 3 additions and 5421 deletions
-5
View File
@@ -67,11 +67,6 @@ file_filter = addons/account_tax_python/i18n/<lang>.po
source_file = addons/account_tax_python/i18n/account_tax_python.pot
source_lang = en
[odoo-master.adyen_platforms]
file_filter = addons/adyen_platforms/i18n/<lang>.po
source_file = addons/adyen_platforms/i18n/adyen_platforms.pot
source_lang = en
[odoo-master.analytic]
file_filter = addons/analytic/i18n/<lang>.po
source_file = addons/analytic/i18n/analytic.pot
-5
View File
@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
-32
View File
@@ -1,32 +0,0 @@
# -*- 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',
'security/ir_rule.xml',
'views/adyen_account_templates.xml',
'views/adyen_account_views.xml',
'views/adyen_bank_account_views.xml',
'views/adyen_shareholder_views.xml',
'views/adyen_transaction_views.xml',
],
'installable': True,
'assets': {
'web.assets_backend': [
'adyen_platforms/static/src/scss/**/*',
'adyen_platforms/static/src/js/**/*',
],
'web.assets_qweb': [
'adyen_platforms/static/src/xml/**/*',
],
},
'license': 'LGPL-3',
}
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import adyen_platforms
@@ -1,45 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from pprint import pformat
from odoo import http
from odoo.http import request
from odoo.addons.adyen_platforms.util import odoo_payments_proxy_control
_logger = logging.getLogger(__name__)
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')
@odoo_payments_proxy_control
@http.route('/adyen_platforms/account_notification', type='json', auth='public', csrf=False)
def adyen_platforms_notification(self):
data = request.jsonrequest
_logger.debug('Account notification received: %s', pformat(data))
account = request.env['adyen.account'].sudo().search([('adyen_uuid', '=', data['adyen_uuid'])])
if not account:
_logger.error('Received notification for non-existing account: %s', data['adyen_uuid'])
return
account.with_context(update_from_adyen=True)._handle_notification(data)
@odoo_payments_proxy_control
@http.route('/adyen_platforms/transaction_notification', type='json', auth='public', csrf=False)
def adyen_transaction_notification(self):
data = request.jsonrequest
_logger.debug('Transaction notification received: %s', pformat(data))
account = request.env['adyen.account'].sudo().search([('adyen_uuid', '=', data['adyen_uuid'])])
if not account:
_logger.error('Received notification for non-existing account: %s', data['adyen_uuid'])
return
request.env['adyen.transaction'].sudo()._handle_notification(data)
@@ -1,26 +0,0 @@
<?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">Odoo Payments 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>
File diff suppressed because it is too large Load Diff
-10
View File
@@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import adyen_mixins
from . import adyen_account
from . import adyen_bank_account
from . import adyen_kyc
from . import adyen_shareholder
from . import adyen_transaction
from . import res_company
@@ -1,647 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import uuid
from datetime import timedelta
import requests
from ast import literal_eval
from dateutil.parser import parse
from pytz import UTC
from werkzeug.urls import url_join
from odoo import _, api, fields, models
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.http import request
from odoo.osv import expression
from odoo.tools import format_amount, format_datetime
from odoo.addons.adyen_platforms.util import AdyenProxyAuth, to_major_currency
_logger = logging.getLogger(__name__)
TIMEOUT = 60
ADYEN_STATUS_MAP = {
'Active': 'active',
'Inactive': 'inactive',
'Suspended': 'suspended',
'Closed': 'closed',
}
ADYEN_VALIDATION_MAP = {
'FAILED': 'failed',
'INVALID_DATA': 'awaiting_data',
'RETRY_LIMIT_REACHED': 'awaiting_data',
'AWAITING_DATA': 'awaiting_data',
'DATA_PROVIDED': 'data_provided',
'PENDING': 'pending',
'PASSED': 'passed',
}
ADYEN_PAYOUT_FREQUENCIES = {
'daily': 'DAILY',
'weekly': 'WEEKLY',
'biweekly': 'BIWEEKLY_ON_1ST_AND_15TH_AT_MIDNIGHT',
'monthly': 'MONTHLY',
}
class AdyenAccount(models.Model):
_name = 'adyen.account'
_inherit = ['mail.thread', 'adyen.id.mixin', 'adyen.address.mixin']
_description = 'Adyen for Platforms 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)
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')
transaction_payout_ids = fields.One2many('adyen.transaction.payout', 'adyen_account_id')
is_business = fields.Boolean('Is a business', required=True)
# Payout
account_code = fields.Char('Account Code')
payout_schedule = fields.Selection([
('daily', 'Daily'),
('weekly', 'Weekly'),
('biweekly', 'Bi-weekly'),
('monthly', 'Monthly'),
], default='biweekly', required=True, string='Payout Schedule')
next_scheduled_payout = fields.Datetime('Next Scheduled Payout', readonly=True)
last_sync_date = fields.Datetime(default=fields.Datetime.now)
# 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')
# Adyen Account Status - internal use
account_status = fields.Selection(string='Internal Account Status', selection=[
('active', 'Active'),
('inactive', 'Inactive'),
('suspended', 'Suspended'),
('closed', 'Closed'),
], default='inactive', readonly=True)
payout_allowed = fields.Boolean(readonly=True)
# Status for UX
state = fields.Selection([
('pending', 'Pending Validation'), # Odoo Validation
('awaiting_data', 'Data To Provide'),
('validation', 'Data Validation'), # KYC Validation from Adyen
('validated', 'Validated'),
], compute='_compute_state', string='Account State')
# KYC
adyen_kyc_ids = fields.One2many('adyen.kyc', 'adyen_account_id', string='KYC Checks', readonly=True)
kyc_tier = fields.Integer(string='KYC Tier', default=0, readonly=True)
kyc_status_message = fields.Html(compute='_compute_kyc_status')
_sql_constraints = [
('adyen_uuid_uniq', 'UNIQUE(adyen_uuid)', 'Adyen UUID should be unique'),
]
def default_get(self, fields):
res = super().default_get(fields)
company_fields = {
'country_id': 'country_id',
'state_id': 'state_id',
'city': 'city',
'zip': 'zip',
'street': 'street',
'email': 'email',
'phone_number': 'phone',
}
if self.env.company.partner_id.is_company:
company_fields.update({
'registration_number': 'vat',
'legal_business_name': 'name',
'doing_business_as': 'name',
})
if 'is_business' in fields:
res['is_business'] = True
field_keys = company_fields.keys() & set(fields)
for field_name in field_keys:
res[field_name] = self.env.company[company_fields[field_name]]
if not self.env.company.partner_id.is_company and {'last_name', 'first_name'} & set(fields):
name = self.env.company.partner_id.name.split()
res['last_name'] = name[-1]
res['first_name'] = ' '.join(name[:-1])
return res
@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.depends('kyc_tier', 'adyen_kyc_ids', 'account_status')
def _compute_state(self):
for account in self:
if account.account_status in ['inactive', 'suspended'] and account.kyc_tier == 0:
account.state = 'pending'
else:
if any(k.status == 'awaiting_data' for k in account.adyen_kyc_ids):
account.state = 'awaiting_data'
elif any(k.status == 'pending' for k in account.adyen_kyc_ids):
account.state = 'validation'
else:
account.state = 'validated'
@api.depends_context('lang')
@api.depends('adyen_kyc_ids')
def _compute_kyc_status(self):
self.kyc_status_message = False
doc_types = dict(self.env['adyen.kyc']._fields['verification_type']._description_selection(self.env))
for account in self.filtered('adyen_kyc_ids.status_message'):
checks = {}
for kyc in account.adyen_kyc_ids.filtered('status_message'):
doc_type = doc_types.get(kyc.verification_type, _('Other'))
checks.setdefault(doc_type, []).append({
'document': kyc.document,
'message': kyc.status_message,
})
account.kyc_status_message = self.env['ir.qweb']._render('adyen_platforms.kyc_status_message', {
'checks': checks
})
@api.model
def create(self, values):
adyen_account_id = super().create(values)
# Create account on odoo.com, proxy and Adyen
create_data = self._format_data(values)
create_data['payoutSchedule'] = ADYEN_PAYOUT_FREQUENCIES.get(values.get('payout_schedule', 'biweekly'), 'BIWEEKLY_ON_1ST_AND_15TH_AT_MIDNIGHT'),
response = self._adyen_rpc('v1/create_account_holder', create_data)
adyen_account_id.with_context(update_from_adyen=True).write({
'account_code': response['adyen_response']['accountCode'],
'adyen_uuid': response['adyen_uuid'],
'proxy_token': response['proxy_token'],
})
self.env.company.adyen_account_id = adyen_account_id.id
return adyen_account_id
def write(self, vals):
adyen_fields = {
'country_id', 'state_id', 'city', 'zip', 'street', 'house_number_or_name', 'email', 'phone_number',
'is_business', 'legal_business_name', 'doing_business_as', 'registration_number', 'first_name',
'last_name', 'date_of_birth', 'document_number', 'document_type',
}
res = super().write(vals)
if vals.keys() & adyen_fields and not self.env.context.get('update_from_adyen'):
self._adyen_rpc('v1/update_account_holder', self._format_data(vals))
if 'payout_schedule' in vals:
self._update_payout_schedule(vals['payout_schedule'])
return res
def unlink(self):
self.check_access_rights('unlink')
for adyen_account_id in self:
adyen_account_id._adyen_rpc('v1/close_account_holder', {
'accountHolderCode': adyen_account_id.account_holder_code,
})
return super().unlink()
def _update_payout_schedule(self, payout_schedule):
self.ensure_one()
self._adyen_rpc('v1/update_payout_schedule', {
'accountCode': self.account_code,
'metadata': {
'adyen_uuid': self.adyen_uuid,
},
'payoutSchedule': {
'action': 'UPDATE',
'schedule': ADYEN_PAYOUT_FREQUENCIES.get(payout_schedule),
}
})
@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.company.get_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):
action = self.env['ir.actions.actions']._for_xml_id('adyen_platforms.adyen_transaction_action')
action['domain'] = expression.AND([[('adyen_account_id', '=', self.id)], literal_eval(action.get('domain', '[]'))])
return action
def _upload_photo_id(self, document_type, content, filename):
test_mode = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.test_mode')
self._adyen_rpc('v1/upload_document', {
'documentDetail': {
'accountHolderCode': self.account_holder_code,
'documentType': document_type,
'filename': filename,
'description': 'PASSED' if test_mode else '',
},
'documentContent': content.decode(),
})
def _format_data(self, values):
fields = values.keys()
data = {
'accountHolderCode': values.get('account_holder_code') or self.account_holder_code,
}
holder_details = {}
# *ALL* the address fields are required if one of them changes
address_fields = {'country_id', 'state_id', 'city', 'zip', 'street', 'house_number_or_name'}
if address_fields & fields:
country_id = self.env['res.country'].browse(values.get('country_id')) if values.get('country_id') else self.country_id
state_id = self.env['res.country.state'].browse(values.get('state_id')) if values.get('state_id') else self.state_id
holder_details['address'] = {
'country': country_id.code,
'stateOrProvince': state_id.code or None,
'city': values.get('city') or self.city,
'postalCode': values.get('zip') or self.zip,
'street': values.get('street') or self.street,
'houseNumberOrName': values.get('house_number_or_name') or self.house_number_or_name,
}
if 'email' in values:
holder_details['email'] = values['email']
if 'phone_number' in values:
holder_details['fullPhoneNumber'] = values['phone_number']
if 'is_business' in values:
data['legalEntity'] = 'Business' if values['is_business'] else 'Individual'
if (values.get('is_business') or self.is_business) and {'legal_business_name', 'doing_business_as', 'registration_number'} & fields:
business_details = holder_details['business_details'] = {}
for source, dest in [
('legal_business_name', 'legalBusinessName'),
('doing_business_as', 'doingBusinessAs'),
('registration_number', 'registrationNumber'),
]:
if source in values:
business_details[dest] = values[source]
elif {'first_name', 'last_name', 'date_of_birth', 'document_number', 'document_type'} & fields:
holder_details['individualDetails'] = {}
if {'first_name', 'last_name'} & fields:
holder_details['individualDetails']['name'] = {
'firstName': values.get('first_name') or self.first_name,
'lastName': values.get('last_name') or self.last_name
}
if 'date_of_birth' in fields:
holder_details['individualDetails'].setdefault('personalData', {})['dateOfBirth'] = str(values['date_of_birth'])
document_number = values.get('document_number') or self.document_number
if self.document_number and 'document_number' in fields:
holder_details['individualDetails'].setdefault('personalData', {})['documentData'] = [{
'number': document_number,
'type': values.get('document_type') or self.document_type,
}]
if holder_details:
data['accountHolderDetails'] = holder_details
return data
def _adyen_rpc(self, operation, adyen_data=None):
adyen_data = adyen_data or {}
if operation == 'v1/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'),
'base_url': self.get_base_url(),
'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 occurred while trying to reach the Adyen proxy.'))
except Exception:
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:
_logger.warning('Proxy error: %s', response['error'])
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.env['adyen.account'].search([]).sync_transactions()
def _handle_notification(self, notification_data):
self.ensure_one()
content = notification_data.get('content', {})
event_type = notification_data.get('eventType')
if event_type == 'ODOO_ACCOUNT_STATUS_CHANGE':
self._handle_odoo_account_status_change(content)
elif event_type == 'ACCOUNT_HOLDER_STATUS_CHANGE':
self._handle_account_holder_status_change_notification(content)
elif event_type == 'ACCOUNT_HOLDER_VERIFICATION':
self._handle_account_holder_verification_notification(content)
elif event_type == 'ACCOUNT_UPDATED':
self._handle_account_updated_notification(content)
elif event_type == 'ACCOUNT_HOLDER_PAYOUT':
self._handle_account_holder_payout(content)
else:
_logger.warning(_("Unknown eventType received: %s", event_type))
def _handle_odoo_account_status_change(self, content):
self.ensure_one()
new_status = content.get('newStatus')
if new_status == 'active' and self.account_status in ['suspended', 'inactive']:
self._adyen_rpc('v1/unsuspend_account_holder', {
'accountHolderCode': self.account_holder_code,
})
elif new_status == 'rejected':
self._adyen_rpc('v1/close_account_holder', {
'accountHolderCode': self.account_holder_code,
})
def _handle_account_holder_status_change_notification(self, content):
self.ensure_one()
# Account Status
new_status = ADYEN_STATUS_MAP.get(content.get('newStatus', {}).get('status'))
if new_status and new_status != self.account_status:
self.account_status = new_status
# Tier
tier = content.get('newStatus', {}).get('processingState', {}).get('tierNumber', None)
if isinstance(tier, int) and tier != self.kyc_tier:
self.kyc_tier = tier
# Payout
payout_allowed = content.get('newStatus', {}).get('payoutState', {}).get('allowPayout', None)
if payout_allowed is not None:
self.payout_allowed = payout_allowed == 'true'
# Events
events = content.get('newStatus', {}).get('events')
if events:
reasons = []
for event in events:
account_event = event.get('AccountEvent', {}).get('reason')
if account_event:
reasons.append(account_event)
status_message = self.env['ir.qweb']._render('adyen_platforms.status_message', {
'message': content.get('reason'),
'reasons': reasons,
})
self.sudo().message_post(body=status_message, subtype_xmlid="mail.mt_comment")
def _handle_account_holder_verification_notification(self, content):
self.ensure_one()
status = ADYEN_VALIDATION_MAP.get(content.get('verificationStatus'))
document = '_'.join(content.get('verificationType', '').lower().split('_')[:-1]) # bank_account, identity, passport, etc.
status_message = content.get('statusSummary', {}).get('kycCheckDescription')
bank_uuid = content.get('bankAccountUUID')
shareholder_uuid = content.get('shareholderCode')
kyc = self.adyen_kyc_ids.filtered(lambda k: k.verification_type == document)
if bank_uuid:
kyc = kyc.filtered(lambda k: k.bank_account_id.bank_account_uuid == bank_uuid or not k.bank_account_id)
elif shareholder_uuid:
kyc = kyc.filtered(lambda k: k.shareholder_id.shareholder_uuid == shareholder_uuid or not k.shareholder_id)
else:
kyc = kyc.filtered(lambda k: not k.shareholder_id and not k.bank_account_id)
if not kyc:
additional_data = {}
if document == 'bank_account' and bank_uuid:
bank_account_id = self.env['adyen.bank.account'].sudo().search([('bank_account_uuid', '=', bank_uuid)])
additional_data['bank_account_id'] = bank_account_id.id
if shareholder_uuid:
shareholder_id = self.env['adyen.shareholder'].sudo().search([('shareholder_uuid', '=', shareholder_uuid)])
additional_data['shareholder_id'] = shareholder_id.id
self.env['adyen.kyc'].sudo().create({
'verification_type': document,
'adyen_account_id': self.id,
'status': status,
'status_message': status_message,
'last_update': fields.Datetime.now(),
**additional_data
})
else:
if bank_uuid and not kyc.bank_account_id:
bank_account_id = self.env['adyen.bank.account'].sudo().search([('bank_account_uuid', '=', bank_uuid)])
kyc.bank_account_id = bank_account_id.id
if shareholder_uuid and not kyc.shareholder_id:
shareholder_id = self.env['adyen.shareholder'].sudo().search([('shareholder_uuid', '=', shareholder_uuid)])
kyc.shareholder_id = shareholder_id.id
if status != kyc.status:
kyc.write({
'status': status,
'status_message': status_message,
'last_update': fields.Datetime.now(),
})
def _handle_account_updated_notification(self, content):
self.ensure_one()
scheduled_date = content.get('payoutSchedule', {}).get('nextScheduledPayout')
if scheduled_date:
self.next_scheduled_payout = parse(scheduled_date).astimezone(UTC).replace(tzinfo=None)
def _handle_account_holder_payout(self, content):
self.ensure_one()
status = content.get('status', {}).get('statusCode')
if status == 'Failed':
status_message = _('Failed payout: %s', content['status']['message']['text'])
self.sudo().message_post(body=status_message, subtype_xmlid="mail.mt_comment")
def sync_transactions(self):
updated_transactions = self.env['adyen.transaction']
for account in self:
page = 1
next_page = True
max_create_date = False
while next_page:
transactions, next_page = account._fetch_transactions(page)
page += 1
for transaction in transactions:
create_date = parse(transaction.get('creationDate')).astimezone(UTC).replace(tzinfo=None)
if not max_create_date:
max_create_date = create_date
if create_date <= account.last_sync_date:
next_page = False
break
status = transaction.get('transactionStatus')
if status in ['Payout', 'PayoutReversed']:
reference = transaction.get('pspReference') or transaction.get('disputePspReference')
tx_sudo = account.transaction_payout_ids.sudo().filtered(lambda t: t.reference == reference)
if not tx_sudo:
self.env['adyen.transaction.payout'].sudo()._create_missing_payout(account.id, transaction)
else:
tx_sudo.status = status
else:
if status in ['Chargeback', 'ChargebackReceived']:
transaction['pspReference'] = transaction.get('disputePspReference')
tx_sudo, _reference, _capture_reference = account.transaction_ids.sudo()._get_tx_from_notification(transaction)
if not tx_sudo:
tx_sudo = self.env['adyen.transaction'].sudo()._create_missing_tx(account.id, transaction)
tx_sudo._update_status(status, create_date)
updated_transactions |= tx_sudo
account.last_sync_date = max_create_date
updated_transactions.sudo()._post_transaction_sync()
def _fetch_transactions(self, page=1):
self.ensure_one()
response = self._adyen_rpc('v1/get_transactions', {
'accountHolderCode': self.account_holder_code,
'transactionListsPerAccount': [{
'accountCode': self.account_code,
'page': page,
}]
})
transaction_list = response['accountTransactionLists'][0]
return transaction_list['transactions'], transaction_list['hasNextPage']
class AdyenAccountBalance(models.Model):
_name = 'adyen.account.balance'
_description = 'Adyen Account Balance'
adyen_account_id = fields.Many2one('adyen.account', required=True, ondelete='cascade')
currency_id = fields.Many2one('res.currency')
balance = fields.Float(default=0.0)
on_hold = fields.Float(default=0.0)
pending = fields.Float(default=0.0)
@api.model
def get_account_balance(self):
if not self.user_has_groups('base.group_erp_manager'):
raise AccessError(_("You can't access account balance."))
if not self.env.company.adyen_account_id:
return {}
balance_fields = {'balance': 'balance', 'onHoldBalance': 'on_hold', 'pendingBalance': 'pending'}
balances = self.env['adyen.account.balance'].sudo().search([
('adyen_account_id', '=', self.env.company.adyen_account_id.id)
])
delta = fields.Datetime.now() - timedelta(hours=1)
if not balances or any(b.write_date <= delta for b in balances):
response = {}
try:
response = self.env.company.adyen_account_id._adyen_rpc('v1/account_holder_balance', {
'accountHolderCode': self.env.company.adyen_account_id.account_holder_code,
})
except UserError as e:
_logger.warning(_('Cannot update account balance, showing previous values: %s', e))
balances.write({
f: 0 for f in balance_fields.values()
})
for total_balance, adyen_balances in response.get('totalBalance', {}).items():
for balance in adyen_balances:
currency_id = self.env['res.currency'].search([('name', '=', balance.get('currency'))])
bal = balances.filtered(lambda b: b.currency_id == currency_id)
if not bal:
bal = self.env['adyen.account.balance'].sudo().create({
'adyen_account_id': self.env.company.adyen_account_id.id,
'currency_id': currency_id.id,
})
balances |= bal
bal[balance_fields.get(total_balance)] = to_major_currency(balance.get('value', 0), currency_id.decimal_places)
warning_delta = fields.Datetime.now() - timedelta(hours=2)
return [{
'currency': b.currency_id.name,
'balance': format_amount(self.env, b.balance, b.currency_id),
'payout_date': format_datetime(self.env, self.env.company.adyen_account_id.next_scheduled_payout, dt_format='short'),
'last_update_warning': b.write_date <= warning_delta,
'last_update': format_datetime(self.env, b.write_date),
} for b in balances]
@@ -1,159 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import uuid
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.mimetypes import guess_mimetype
from odoo.addons.adyen_platforms.models.adyen_kyc import ADYEN_KYC_STATUS
from odoo.addons.adyen_platforms.models.adyen_mixins import ADYEN_AVAILABLE_COUNTRIES
class AdyenBankAccount(models.Model):
_name = 'adyen.bank.account'
_description = 'Adyen for Platforms 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)
currency_name = fields.Char(related='currency_id.name', string='Currency Name')
iban = fields.Char('IBAN')
account_number = fields.Char('Account Number')
branch_code = fields.Char('Branch Code')
bank_city = fields.Char('Bank City')
bank_code = fields.Char('Bank Code')
bank_name = fields.Char('Bank Name')
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
adyen_kyc_ids = fields.One2many('adyen.kyc', 'bank_account_id')
kyc_status = fields.Selection(ADYEN_KYC_STATUS, compute='_compute_kyc_status', readonly=True)
kyc_status_message = fields.Char(compute='_compute_kyc_status', readonly=True)
@api.depends_context('lang')
@api.depends('adyen_kyc_ids')
def _compute_kyc_status(self):
self.kyc_status_message = False
self.kyc_status = False
for bank_account in self.filtered('adyen_kyc_ids'):
kyc = bank_account.adyen_kyc_ids._sort_by_status()
bank_account.kyc_status = kyc[0].status
@api.model
def create(self, values):
adyen_bank_account_id = super().create(values)
adyen_account_id = self.env['adyen.account'].browse(values.get('adyen_account_id'))
response = adyen_account_id._adyen_rpc('v1/update_account_holder', self._format_data(values))
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().write(vals)
if not self.env.context.get('update_from_adyen'):
self.adyen_account_id._adyen_rpc('v1/update_account_holder', self._format_data(vals))
if 'bank_statement' in vals:
self._upload_bank_statement(vals['bank_statement'], vals['bank_statement_filename'])
return res
def unlink(self):
self.check_access_rights('unlink')
for bank_account_id in self:
bank_account_id.adyen_account_id._adyen_rpc('v1/delete_bank_accounts', {
'accountHolderCode': bank_account_id.adyen_account_id.account_holder_code,
'bankAccountUUIDs': [bank_account_id.bank_account_uuid],
})
return super().unlink()
def name_get(self):
res = []
for bank_account in self:
name = f'{bank_account.owner_name} - f{bank_account.iban or bank_account.account_number}'
res.append((bank_account.id, name))
return res
def _format_data(self, values=None):
if values is None:
values = {}
adyen_account_id = self.env['adyen.account'].browse(values.get('adyen_account_id')) if values.get('adyen_account_id') else self.adyen_account_id
country_id = self.env['res.country'].browse(values.get('country_id')) if values.get('country_id') else self.country_id
currency_id = self.env['res.currency'].browse(values.get('currency_id')) if values.get('currency_id') else self.currency_id
owner_country_id = self.env['res.country'].browse(values.get('owner_country_id')) if values.get('owner_country_id') else self.owner_country_id
owner_state_id = self.env['res.country.state'].browse(values.get('owner_state_id')) if values.get('owner_state_id') else self.owner_state_id
return {
'accountHolderCode': adyen_account_id.account_holder_code,
'accountHolderDetails': {
'bankAccountDetails': [{
'accountNumber': values.get('account_number') or self.account_number or None,
'accountType': values.get('account_type') or self.account_type or None,
'bankAccountReference': values.get('bank_account_reference') or self.bank_account_reference,
'bankAccountUUID': values.get('bank_account_uuid') or self.bank_account_uuid or None,
'bankCity': values.get('bank_city') or self.bank_city or None,
'bankCode': values.get('bank_code') or self.bank_code or None,
'bankName': values.get('bank_name') or self.bank_name or None,
'branchCode': values.get('branch_code') or self.branch_code or None,
'countryCode': country_id.code,
'currencyCode': currency_id.name,
'iban': values.get('iban') or self.iban or None,
'ownerCity': values.get('owner_city') or self.owner_city or None,
'ownerCountryCode': owner_country_id.code or None,
'ownerHouseNumberOrName': values.get('owner_house_number_or_name') or self.owner_house_number_or_name or None,
'ownerName': values.get('owner_name') or self.owner_name,
'ownerPostalCode': values.get('owner_zip') or self.owner_zip or None,
'ownerState': owner_state_id.code or None,
'ownerStreet': values.get('owner_street') or self.owner_street or None,
}],
}
}
def _upload_bank_statement(self, content, filename):
content_encoded = content.encode('utf8')
mimetype = guess_mimetype(base64.b64decode(content_encoded))
file_size = len(content_encoded)
# Document requirements: https://docs.adyen.com/platforms/verification-checks/bank-account-check#requirements
if mimetype not in ['image/jpeg', 'image/png', 'application/pdf']:
raise ValidationError(_('Allowed file formats for bank statements are jpeg, jpg, pdf or png. Received %r instead.', mimetype))
if file_size < (100 * 1024) or (file_size < 1024 and mimetype == 'application/pdf'):
raise ValidationError(_('Minimum allowed size for bank statements: 1 KB for PDF, 100 KB for other formats.'))
if file_size > (4 * 1024 * 1024):
raise ValidationError(_('Maximum allowed size for bank statements: 4MB.'))
test_mode = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.test_mode')
self.adyen_account_id._adyen_rpc('v1/upload_document', {
'documentDetail': {
'accountHolderCode': self.adyen_account_id.account_holder_code,
'bankAccountUUID': self.bank_account_uuid,
'documentType': 'BANK_STATEMENT',
'filename': filename,
'description': 'PASSED' if test_mode else '',
},
'documentContent': content,
})
@@ -1,54 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
ADYEN_KYC_STATUS = [
('awaiting_data', 'Data To Provide'),
('pending', 'Waiting For Validation'),
('data_provided', 'Data Provided'),
('passed', 'Confirmed'),
('failed', 'Failed'),
]
class AdyenKYC(models.Model):
_name = 'adyen.kyc'
_description = 'Adyen KYC checks'
status_message = fields.Char()
status = fields.Selection(string='KYC Status', selection=[
('awaiting_data', 'Data To Provide'),
('pending', 'Waiting For Validation'),
('data_provided', 'Data Provided'),
('passed', 'Confirmed'),
('failed', 'Failed'),
], required=True, default='pending')
adyen_account_id = fields.Many2one('adyen.account', required=True, ondelete='cascade')
bank_account_id = fields.Many2one(
'adyen.bank.account', domain="[('adyen_account_id', '=', adyen_account_id)]", ondelete='cascade')
shareholder_id = fields.Many2one(
'adyen.shareholder', domain="[('adyen_account_id', '=', adyen_account_id)]", ondelete='cascade')
document = fields.Char(compute='_compute_document')
verification_type = fields.Selection([
('company', 'Company'),
('identity', 'Identity'),
('passport', 'Passport'),
('bank_account', 'Bank Account'),
('nonprofit', 'Nonprofit'),
('card', 'Card'),
], string='KYC Document')
last_update = fields.Datetime(string="Last Update")
def _sort_by_status(self):
order = ['failed', 'awaiting_data', 'pending', 'data_provided', 'passed']
kyc_sorted = sorted(self, key=lambda k: order.index(k.status))
return kyc_sorted
@api.depends('bank_account_id', 'shareholder_id')
def _compute_document(self):
self.document = False
for kyc in self.filtered(lambda k: k.bank_account_id or k.shareholder_id):
kyc.document = kyc.bank_account_id.display_name or kyc.shareholder_id.display_name
@@ -1,81 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.mimetypes import guess_mimetype
ADYEN_AVAILABLE_COUNTRIES = [
'US', 'AT', 'AU', 'BE', 'CA', 'CH', 'CZ', 'DE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'IE', 'IT',
'LT', 'LU', 'NL', 'PL', 'PT'
]
class AdyenAddressMixin(models.AbstractModel):
_name = 'adyen.address.mixin'
_description = 'Adyen for Platforms 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().write(vals)
# Check file formats
if vals.get('id_front'):
self._check_file_requirements(vals.get('id_front'))
if vals.get('id_back'):
self._check_file_requirements(vals.get('id_back'))
for adyen_account in self:
if vals.get('id_front'):
document_type = adyen_account.id_type
if adyen_account.id_type in ['ID_CARD', 'DRIVING_LICENSE']:
document_type += '_FRONT'
adyen_account._upload_photo_id(document_type, adyen_account.id_front, adyen_account.id_front_filename)
if vals.get('id_back') and adyen_account.id_type in ['ID_CARD', 'DRIVING_LICENSE']:
document_type = adyen_account.id_type + '_BACK'
adyen_account._upload_photo_id(document_type, adyen_account.id_back, adyen_account.id_back_filename)
return res
@api.model
def _check_file_requirements(self, content):
content_encoded = content.encode('utf8')
mimetype = guess_mimetype(base64.b64decode(content_encoded))
file_size = len(content_encoded)
# Document requirements: https://docs.adyen.com/platforms/verification-checks/photo-id-check#requirements
if mimetype not in ['image/jpeg', 'image/png', 'application/pdf']:
raise ValidationError(_('Allowed file formats for photo IDs are jpeg, jpg, pdf or png'))
if file_size < (100 * 1024) or (file_size < 1024 and mimetype == 'application/pdf'):
raise ValidationError(_('Minimum allowed size for photo ID: 1 KB for PDF, 100 KB for other formats.'))
if file_size > (4 * 1024 * 1024):
raise ValidationError(_('Maximum allowed size for photo ID: 4 MB.'))
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()
@@ -1,129 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import uuid
from odoo import api, fields, models
from odoo.addons.adyen_platforms.models.adyen_kyc import ADYEN_KYC_STATUS
class AdyenShareholder(models.Model):
_name = 'adyen.shareholder'
_inherit = ['adyen.id.mixin', 'adyen.address.mixin']
_description = 'Adyen for Platforms 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")
adyen_kyc_ids = fields.One2many('adyen.kyc', 'shareholder_id')
kyc_status = fields.Selection(ADYEN_KYC_STATUS, compute='_compute_kyc_status', readonly=True)
kyc_status_message = fields.Char(compute='_compute_kyc_status', readonly=True)
@api.depends_context('lang')
@api.depends('adyen_kyc_ids')
def _compute_kyc_status(self):
self.kyc_status_message = False
self.kyc_status = False
for shareholder in self.filtered('adyen_kyc_ids'):
kyc = shareholder.adyen_kyc_ids._sort_by_status()
shareholder.kyc_status = kyc[0].status
@api.depends('first_name', 'last_name')
def _compute_full_name(self):
for adyen_shareholder_id in self:
adyen_shareholder_id.full_name = f'{adyen_shareholder_id.first_name} {adyen_shareholder_id.last_name}'
@api.model
def create(self, values):
adyen_shareholder_id = super().create(values)
adyen_account_id = self.env['adyen.account'].browse(values.get('adyen_account_id'))
response = adyen_account_id._adyen_rpc('v1/update_account_holder', self._format_data(values))
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().write(vals)
if not self.env.context.get('update_from_adyen'):
self.adyen_account_id._adyen_rpc('v1/update_account_holder', self._format_data(vals))
return res
def unlink(self):
self.check_access_rights('unlink')
for shareholder_id in self:
shareholder_id.adyen_account_id._adyen_rpc('v1/delete_shareholders', {
'accountHolderCode': shareholder_id.adyen_account_id.account_holder_code,
'shareholderCodes': [shareholder_id.shareholder_uuid],
})
return super().unlink()
def _upload_photo_id(self, document_type, content, filename):
test_mode = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.test_mode')
self.adyen_account_id._adyen_rpc('v1/upload_document', {
'documentDetail': {
'accountHolderCode': self.adyen_account_id.account_holder_code,
'shareholderCode': self.shareholder_uuid,
'documentType': document_type,
'filename': filename,
'description': 'PASSED' if test_mode else '',
},
'documentContent': content.decode(),
})
def _format_data(self, values):
adyen_account_id = self.env['adyen.account'].browse(values.get('adyen_account_id')) if values.get('adyen_account_id') else self.adyen_account_id
country_id = self.env['res.country'].browse(values.get('country_id')) if values.get('country_id') else self.country_id
state_id = self.env['res.country.state'].browse(values.get('owner_state_id')) if values.get('state_id') else self.state_id
data = {
'accountHolderCode': adyen_account_id.account_holder_code,
'accountHolderDetails': {
'businessDetails': {
'shareholders': [{
'shareholderCode': values.get('shareholder_uuid') or self.shareholder_uuid or None,
'shareholderReference': values.get('shareholder_reference') or self.shareholder_reference,
'address': {
'city': values.get('city') or self.city,
'country': country_id.code,
'houseNumberOrName': values.get('house_number_or_name') or self.house_number_or_name,
'postalCode': values.get('zip') or self.zip,
'stateOrProvince': state_id.code or None,
'street': values.get('street') or self.street,
},
'name': {
'firstName': values.get('first_name') or self.first_name,
'lastName': values.get('last_name') or self.last_name,
'gender': 'UNKNOWN'
},
'personalData': {
'dateOfBirth': str(values.get('date_of_birth') or self.date_of_birth),
}
}]
}
}
}
# documentData cannot be present in the data if not set
document_number = values.get('document_number') or self.document_number
if document_number:
data['accountHolderDetails']['businessDetails']['shareholders'][0]['personalData']['documentData'] = [{
'number': document_number,
'type': 'ID',
}]
return data
@@ -1,315 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from pytz import UTC
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.osv import expression
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
from odoo.addons.adyen_platforms.util import to_major_currency, to_minor_currency
_logger = logging.getLogger(__name__)
class AdyenTransaction(models.Model):
_name = 'adyen.transaction'
_description = 'Adyen for Platforms Transaction'
_order = 'date desc'
_rec_name = 'reference'
adyen_account_id = fields.Many2one('adyen.account')
reference = fields.Char('Reference', index=True, required=True)
capture_reference = fields.Char('Capture Reference')
total_amount = fields.Float('Customer Amount')
currency_id = fields.Many2one('res.currency', string='Currency')
merchant_amount = fields.Float('Merchant Amount')
fees = fields.Float('Fees')
fixed_fees = fields.Float('Fixed Fees')
variable_fees = fields.Float('Variable Fees')
fees_currency_id = fields.Many2one('res.currency', default=lambda self: self.env.ref('base.EUR'))
date = fields.Datetime('Date')
description = fields.Char('Description')
signature = fields.Char('Signature')
reason = fields.Char('Failure Reason')
status = fields.Selection(related='last_status_id.status')
last_status_id = fields.Many2one('adyen.transaction.status', 'Last Status', compute='_compute_last_status_id', store=True, readonly=True)
last_status_update = fields.Datetime(related='last_status_id.date', string='Last Status Update')
status_ids = fields.One2many('adyen.transaction.status', 'adyen_transaction_id', 'Status History')
payment_method = fields.Char('Payment Method')
shopper_country_id = fields.Many2one('res.country')
card_country_id = fields.Many2one('res.country')
commercial_card = fields.Selection([
('yes', 'Yes'),
('no', 'No'),
('unknown', 'Unknown'),
], default='unknown')
dispute_reference = fields.Char('Dispute Reference')
_sql_constraints = [
('reference_unique', 'unique(reference, capture_reference)', 'A transaction with the same reference already exists.'),
]
@api.depends('status_ids')
def _compute_last_status_id(self):
self.last_status_id = False
for transaction in self.filtered('status_ids'):
transaction.last_status_id = transaction.status_ids.sorted('date')[-1]
def _get_tx_from_notification(self, notification):
if notification.get('eventCode') in ['CAPTURE', 'REFUND']:
reference = notification.get('originalReference')
capture_reference = notification.get('pspReference')
else:
reference = notification.get('pspReference')
capture_reference = notification.get('capturePspReference')
domain = [('reference', '=', reference)]
if capture_reference:
domain = expression.AND([domain, [('capture_reference', '=', capture_reference)]])
if self:
return self.filtered_domain(domain), reference, capture_reference
return self.env['adyen.transaction'].search(domain), reference, capture_reference
@api.model
def _handle_notification(self, data):
adyen_uuid = data.get("additionalData", {}).get("metadata.adyen_uuid") or data.get('adyen_uuid')
account = self.env['adyen.account'].sudo().search([('adyen_uuid', '=', adyen_uuid)])
if not account:
_logger.warning("Received payment notification for non-existing account")
return
tx_sudo, reference, capture_reference = self.env['adyen.transaction']._get_tx_from_notification(data)
if not tx_sudo:
tx_sudo = self.env['adyen.transaction'].sudo().create({
'adyen_account_id': account.id,
'reference': reference,
'capture_reference': capture_reference,
'description': data.get('merchantReference'),
})
event_code = data.get('eventCode')
if event_code == "AUTHORISATION":
tx_sudo._handle_authorisation_notification(data)
elif event_code == "FEES_UPDATED":
tx_sudo._handle_fees_updated_notification(data)
elif event_code == "REFUND":
tx_sudo._handle_refund_notification(data)
elif event_code in ["CHARGEBACK", "NOTIFICATION_OF_CHARGEBACK"]:
tx_sudo._handle_chargeback_notification(data)
else:
_logger.warning(_("Unknown eventCode received: %s", event_code))
return tx_sudo
def _handle_authorisation_notification(self, notification_data):
self.ensure_one()
additional_data = notification_data.get('additionalData', {})
currency_id = self.env['res.currency'].search([('name', '=', notification_data.get('amount', {}).get('currency'))])
shopper_country_id = self.env['res.country'].search([('code', '=', additional_data.get('shopperCountry'))])
commercial_card = additional_data.get('isCardCommercial', 'unknown')
card_country_id = self.env['res.country'].search([('code', '=', additional_data.get('cardIssuingCountry', additional_data.get('issuerCountry')))])
self.write({
'reference': notification_data.get('pspReference'),
'total_amount': to_major_currency(notification_data['amount']['value'], currency_id.decimal_places),
'currency_id': currency_id.id,
'date': parse(notification_data.get('eventDate')).astimezone(UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'description': notification_data.get('merchantReference'),
'payment_method': notification_data.get('paymentMethod'),
'shopper_country_id': shopper_country_id.id,
'card_country_id': card_country_id.id,
'commercial_card': commercial_card if commercial_card in ('yes', 'no', 'unknown') else 'unknown',
})
self._trigger_sync()
def _handle_fees_updated_notification(self, notification_data):
self.ensure_one()
currency_amount = self.env['res.currency'].search([('name', '=', notification_data.get('totalAmount', {}).get('currency'))])
currency_fees = self.env['res.currency'].search([('name', '=', notification_data.get('totalFees', {}).get('currency'))])
self.capture_reference = notification_data.get('captureReference')
self.fees = to_major_currency(notification_data['totalFees']['value'], currency_fees.decimal_places)
self.fixed_fees = to_major_currency(notification_data['fixedFees']['value'], currency_fees.decimal_places)
self.variable_fees = to_major_currency(notification_data['variableFees']['value'], currency_fees.decimal_places)
self.merchant_amount = to_major_currency(notification_data['merchantAmount']['value'], currency_amount.decimal_places)
self.total_amount = to_major_currency(notification_data['totalAmount']['value'], currency_amount.decimal_places)
self.signature = notification_data.get('signature')
def _handle_refund_notification(self, notification_data):
self.ensure_one()
currency_id = self.env['res.currency'].search([('name', '=', notification_data.get('amount', {}).get('currency'))])
self.currency_id = currency_id.id
self.date = parse(notification_data.get('eventDate')).astimezone(UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
reason = notification_data.get('reason')
if reason:
self.reason = reason
def _handle_chargeback_notification(self, notification_data):
self.ensure_one()
self.dispute_reference = notification_data.get('pspReference')
self.adyen_account_id.message_post(
body=_('Transaction %s has been CHARGEBACK\'ed: %s', self.description or self.reference, notification_data.get('reason')),
subtype_xmlid="mail.mt_comment"
)
def _create_missing_tx(self, account_id, transaction, **kwargs):
currency_id = self.env['res.currency'].search([('name', '=', transaction.get('amount', {}).get('currency'))])
amount = to_major_currency(transaction['amount']['value'], currency_id.decimal_places)
tx = self.create({
'adyen_account_id': account_id,
'reference': transaction.get('pspReference'),
'capture_reference': transaction.get('capturePspReference'),
'merchant_amount': amount,
'total_amount': amount,
'currency_id': currency_id.id,
'date': parse(transaction.get('creationDate')).astimezone(UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'description': transaction.get('description'),
**kwargs,
})
return tx
def _trigger_sync(self):
sync_cron = self.env.ref('adyen_platforms.adyen_sync_cron', raise_if_not_found=False)
if sync_cron:
sync_cron._trigger(at=fields.Datetime.now() + relativedelta(minutes=5))
def _update_status(self, new_status, date):
self.ensure_one()
if not self.status_ids or self.status_ids[0].date != date:
self.status_ids = [(0, 0, {
'adyen_transaction_id': self.id,
'status': new_status,
'date': date,
})]
def _post_transaction_sync(self):
""" Hook defined to perform actions on transactions after they were sync'ed """
return
def _refund_request(self, amount=None):
self.ensure_one()
if amount is None:
amount = self.total_amount
if amount > self.total_amount:
raise ValidationError(_('You cannot refund more than the original amount.'))
converted_amount = to_minor_currency(amount, self.currency_id.decimal_places)
initial_amount = to_minor_currency(self.total_amount, self.currency_id.decimal_places)
fees_amount = to_minor_currency(self.fees, self.fees_currency_id.decimal_places)
refund_data = {
'originalReference': self.reference,
'modificationAmount': {
'currency': self.currency_id.name,
'value': converted_amount,
},
'initialAmount': {
'currency': self.currency_id.name,
'value': initial_amount,
},
'feesAmount': {
'currency': self.fees_currency_id.name,
'value': fees_amount,
},
'date': str(self.date),
'reference': 'Refund of %s' % self.description, # TODO generate unique reference
'payout': self.adyen_account_id.account_code,
'adyen_uuid': self.adyen_account_id.adyen_uuid,
'signature': self.signature,
}
res = self.adyen_account_id._adyen_rpc('v1/refund', refund_data)
refund_tx = self.env['adyen.transaction'].sudo().create({
'adyen_account_id': self.adyen_account_id.id,
'reference': self.reference,
'capture_reference': res['pspReference'],
'description': refund_data.get('reference'),
'currency_id': self.currency_id.id,
'total_amount': to_major_currency(res['totalAmount']['value'], self.currency_id.decimal_places),
'fees': to_major_currency(res['totalFees']['value'], self.currency_id.decimal_places),
'variable_fees': to_major_currency(res['totalFees']['value'], self.currency_id.decimal_places),
'merchant_amount': to_major_currency(res['merchantAmount']['value'], self.currency_id.decimal_places),
'date': fields.Datetime.now(),
})
self._trigger_sync()
return refund_tx
def action_refund(self):
for tx in self:
tx._refund_request()
class AdyenTransactionStatus(models.Model):
_name = 'adyen.transaction.status'
_description = 'Transaction Status'
_order = 'date desc'
_rec_name = 'status'
adyen_transaction_id = fields.Many2one('adyen.transaction', required=True, ondelete='cascade')
status = fields.Selection(string='Status', selection=[
('unknown', 'Unknown'),
('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'),
('FundTransfer', 'Fund Transfer'),
('PendingFundTransfer', 'Pending Fund Transfer'),
('ManualCorrected', 'Manual Corrected'),
])
date = fields.Datetime()
class AdyenTransactionPayout(models.Model):
_name = 'adyen.transaction.payout'
_description = 'Payout Transaction'
_order = 'date desc'
adyen_account_id = fields.Many2one('adyen.account')
date = fields.Datetime()
amount = fields.Float('Amount', required=True)
currency_id = fields.Many2one('res.currency', required=True)
reference = fields.Char('Reference', index=True, required=True)
bank_account_id = fields.Many2one('adyen.bank.account')
status = fields.Selection(string='Type', selection=[
('unknown', 'Unknown'),
('Payout', 'Payout'),
('PayoutReversed', 'Payout Reversed'),
], default='unknown')
def _create_missing_payout(self, account_id, transaction, **kwargs):
currency_id = self.env['res.currency'].search([('name', '=', transaction['amount']['currency'])])
bank_account_id = self.env['adyen.bank.account'].search([('bank_account_uuid', '=', transaction.get('bankAccountDetail', {}).get('bankAccountUUID'))])
tx = self.create({
'adyen_account_id': account_id,
'date': parse(transaction.get('creationDate')).astimezone(UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'amount': to_major_currency(transaction.get('amount', {}).get('value'), currency_id.decimal_places),
'currency_id': currency_id.id,
'reference': transaction.get('pspReference'),
'bank_account_id': bank_account_id.id,
'status': transaction.get('transactionStatus'),
**kwargs,
})
return tx
@@ -1,9 +0,0 @@
# 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)
@@ -1,9 +0,0 @@
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_transaction_group_erp_manager,adyen.transaction,model_adyen_transaction,base.group_erp_manager,1,0,0,0
access_adyen_kyc_group_erp_manager,adyen.kyc,model_adyen_kyc,base.group_erp_manager,1,0,0,0
access_adyen_account_balance_group_erp_manager,adyen.account.balance,model_adyen_account_balance,base.group_erp_manager,1,0,0,0
access_adyen_transaction_status_group_erp_manager,adyen.transaction.status,model_adyen_transaction_status,base.group_erp_manager,1,0,0,0
access_adyen_transaction_payout_group_erp_manager,adyen.transaction.payout,model_adyen_transaction_payout,base.group_erp_manager,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_adyen_account_group_erp_manager adyen.account model_adyen_account base.group_erp_manager 1 1 1 1
3 access_adyen_shareholder_group_erp_manager adyen.shareholder model_adyen_shareholder base.group_erp_manager 1 1 1 1
4 access_adyen_bank_account_group_erp_manager adyen.bank.account model_adyen_bank_account base.group_erp_manager 1 1 1 1
5 access_adyen_transaction_group_erp_manager adyen.transaction model_adyen_transaction base.group_erp_manager 1 0 0 0
6 access_adyen_kyc_group_erp_manager adyen.kyc model_adyen_kyc base.group_erp_manager 1 0 0 0
7 access_adyen_account_balance_group_erp_manager adyen.account.balance model_adyen_account_balance base.group_erp_manager 1 0 0 0
8 access_adyen_transaction_status_group_erp_manager adyen.transaction.status model_adyen_transaction_status base.group_erp_manager 1 0 0 0
9 access_adyen_transaction_payout_group_erp_manager adyen.transaction.payout model_adyen_transaction_payout base.group_erp_manager 1 0 0 0
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="adyen_platforms_account_closed" model="ir.rule">
<field name="model_id" ref="model_adyen_account"/>
<field name="name">Readonly: Closed Accounts</field>
<field name="domain_force">[('account_status', '!=', 'closed')]</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
</odoo>
@@ -1,63 +0,0 @@
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);
});
@@ -1,39 +0,0 @@
odoo.define('adyen_platforms.transactions.dashboard', function (require) {
"use strict";
var ListRenderer = require('web.ListRenderer');
var ListView = require('web.ListView');
var viewRegistry = require('web.view_registry');
var core = require('web.core');
var QWeb = core.qweb;
var AdyenTransactionsListRenderer = ListRenderer.extend({
_render: function () {
const el = this.$el.parent();
return this._super.apply(this, arguments).then(() => {
this._rpc({
model: 'adyen.account.balance',
method: 'get_account_balance',
content: this.context
}).then((result) => {
el.parent().find('.o_adyen_transactions_dashboard').remove();
const dash = QWeb.render('AdyenTransactions.dashboard', {
balances: result,
});
el.before(dash);
});
});
}
});
var AdyenTransactionsListView = ListView.extend({
config: _.extend({}, ListView.prototype.config, {
Renderer: AdyenTransactionsListRenderer
}),
});
viewRegistry.add('adyen_transactions_list', AdyenTransactionsListView);
});
@@ -1,35 +0,0 @@
.o_adyen_transactions_dashboard {
.o_adyen_account_balance_card {
margin: 4px;
flex: 1;
text-align: center;
padding: 4px;
h4 {
color: $o-enterprise-color;
font-weight: bold;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
&:not(:last-child) {
border-bottom: 2px solid $o-enterprise-color;
}
@include media-breakpoint-up(sm) {
&:not(:last-child) {
border-right: 2px solid $o-enterprise-color;
border-bottom: 0 !important;
}
}
.o_primary_amount {
color: $o-enterprise-primary-color;
font-size: 2rem;
}
}
}
@@ -1,125 +0,0 @@
<?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 additional 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>European consumer cards: 1.3% + 0.20€</li>
<li>Commercial and non-EU cards: 2.8% + 0.20€</li>
</ul>
<input type="checkbox" class="opt_in_checkbox" id="optin"/>
<label for="optin">
I confirm I have taken notice of and accept the following terms and restrictions:
</label>
<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>
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="AdyenTransactions.dashboard">
<div class="o_adyen_transactions_dashboard">
<div class="d-flex justify-content-around flex-column flex-sm-row">
<t t-foreach="balances" t-as="balance">
<div class="o_adyen_account_balance_card">
<h4 t-attf-title="Last updated on {{balance.last_update}}"><t t-esc="balance.currency" /><t t-if="balance.last_update_warning">*</t> - Balance</h4>
<ul>
<li class="o_primary_amount"><t t-esc="balance.balance" /></li>
<li t-if="balance.payout_date">Next Payout Date: <t t-esc="balance.payout_date" /></li>
</ul>
</div>
</t>
</div>
</div>
</t>
</templates>
-94
View File
@@ -1,94 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import decorator
import hashlib
import hmac
import json
import logging
import requests
import time
from werkzeug.urls import url_decode, url_parse
from werkzeug.exceptions import Forbidden
from odoo.http import request
from odoo.tools import float_round
_logger = logging.getLogger(__name__)
TIMEOUT = 60
def to_major_currency(amount, decimal=2):
return float_round(amount, 0) / (10**decimal)
def to_minor_currency(amount, decimal=2):
return int(float_round(amount, decimal) * (10**decimal))
class AdyenProxyAuth(requests.auth.AuthBase):
def __init__(self, adyen_account_id):
super().__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 = 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(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
@decorator.decorator
def odoo_payments_proxy_control(func, *args, **kwargs):
_logger.debug('Check notification from Odoo Payments')
adyen_uuid = request.httprequest.headers.get('oe-adyen-uuid')
account_id = request.env['adyen.account'].sudo().search([('adyen_uuid', '=', adyen_uuid)])
if not account_id:
raise Forbidden()
secret = account_id.proxy_token.encode('utf8')
msg_signature = request.httprequest.headers.get('oe-signature') # base64 encoded hmac sha256 digest
msg_timestamp = request.httprequest.headers.get('oe-timestamp')
if not (secret and msg_signature and msg_timestamp):
raise Forbidden()
if int(msg_timestamp) + TIMEOUT < int(time.time()):
_logger.debug('HTTP request validation failed due to invalid timestamp for route %r', request.httprequest.path)
raise Forbidden()
h = hmac.new(secret, digestmod=hashlib.sha256)
body = json.dumps(request.jsonrequest, sort_keys=True)
message = '%s|%s|%s|%s' % (
msg_timestamp, # timestamp
request.httprequest.path, # url path
json.dumps(url_decode(request.httprequest.query_string), sort_keys=True), # url query params sorted by key
body, # http request body
)
h.update(message.encode('utf-8'))
if not hmac.compare_digest(h.digest(), base64.b64decode(msg_signature)):
raise Forbidden()
return func(*args, **kwargs)
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="kyc_status_message">
<p t-if="message" t-esc="message" />
<t t-if="checks">
<t t-foreach="checks" t-as="verification_type">
<ul>
<li><strong><t t-esc="verification_type" />:</strong>
<ul>
<t t-foreach="checks[verification_type]" t-as="check">
<li>
<strong t-if="check['document']"><t t-esc="check['document']" />: </strong><t t-esc="check['message']" />
</li>
</t>
</ul>
</li>
</ul>
</t>
</t>
</template>
<template id="status_message">
<p t-if="message"><t t-esc="message" />:</p>
<ul>
<li t-foreach="reasons" t-as="reason">
<t t-esc="reason" />
</li>
</ul>
</template>
</odoo>
@@ -1,174 +0,0 @@
<?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">
<field name="account_code" invisible="1" />
<field name="account_holder_code" invisible="1" />
<field name="account_status" invisible="1" />
<field name="adyen_uuid" invisible="1"/>
<field name="kyc_tier" invisible="1" />
<field name="payout_allowed" invisible="1" />
<header>
<button name="sync_transactions" string="Sync Transactions" type="object" attrs="{'invisible': [('account_code', '=', False)]}" groups="base.group_no_one" />
<field name="state" widget="statusbar" />
</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>
<div class="alert alert-danger" role="alert" attrs="{'invisible': [('account_status', '!=', 'closed')]}">
<p>Your account is Closed.</p>
</div>
<div class="alert alert-warning" role="alert" attrs="{'invisible': [('kyc_status_message', '=', False)]}">
<field name="kyc_status_message" readonly="1" />
</div>
<div class="alert alert-warning" role="alert" attrs="{'invisible': ['|', '|', ('account_code', '=', False), ('state', '=', 'pending'), ('bank_account_ids', '!=', [])]}">
<p>A bank account is required to perform payouts.</p>
</div>
<group>
<group>
<field name="is_business" attrs="{'readonly': [('id', '!=', False)]}"/>
</group>
<group>
<field name="company_id" readonly="1" groups="base.group_multi_company" />
</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_code', 'in', ['AU', 'CA', 'GR', 'IT', 'US'])]}"/>
<field name="document_type"
attrs="{'invisible': [('country_code', '!=', 'AU')], 'required': [('is_business', '=', False), ('country_code', '=', 'AU')]}"/>
<field name="id_type" attrs="{'invisible': [('adyen_uuid', '=', False)], 'required': [('is_business', '=', False), ('kyc_tier', '>', 1)]}"/>
<field name="id_front" filename="id_front_filename" attrs="{'invisible': [('adyen_uuid', '=', False)], 'required': [('is_business', '=', False), ('kyc_tier', '>', 1)]}"/>
<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'])],
'required': [('id_type', 'in', ['ID_CARD', 'DRIVING_LICENSE']), ('kyc_tier', '>=', 1)]}"/>
<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="KYC">
<field name="adyen_kyc_ids">
<tree no_open="1">
<field name="verification_type" />
<field name="document" />
<field name="status" widget="badge"
decoration-warning="status == 'awaiting_data'"
decoration-info="status == 'pending'"
decoration-success="status in ['data_provided', 'passed']"
decoration-danger="status == 'failed'"
/>
<field name="status_message" />
<field name="last_update" />
</tree>
</field>
</page>
<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="badge" attrs="{'invisible': [('kyc_status', '=', False)]}"
decoration-warning="kyc_status == 'awaiting_data'"
decoration-info="kyc_status == 'pending'"
decoration-success="kyc_status in ['data_provided', 'passed']"
decoration-danger="kyc_status == 'failed'"
/>
</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="currency_id"/>
<field name="kyc_status" widget="badge" attrs="{'invisible': [('kyc_status', '=', False)]}"
decoration-warning="kyc_status == 'awaiting_data'"
decoration-info="kyc_status == 'pending'"
decoration-success="kyc_status in ['data_provided', 'passed']"
decoration-danger="kyc_status == 'failed'"
/>
</tree>
</field>
</page>
<page string="Payouts">
<group>
<group>
<field name="payout_schedule" attrs="{'readonly': [('payout_allowed', '=', False)]}" />
</group>
<group>
<field name="next_scheduled_payout" attrs="{'invisible': [('next_scheduled_payout', '=', False)]}" />
</group>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</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>
<record id="adyen_account_action" model="ir.actions.server">
<field name="name">Account Configuration</field>
<field name="model_id" ref="adyen_platforms.model_adyen_account" />
<field name="type">ir.actions.server</field>
<field name="state">code</field>
<field name="code">
action = model.action_create_redirect()
</field>
</record>
</odoo>
@@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<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">
<field name="bank_account_reference" invisible="1" />
<field name="bank_account_uuid" invisible="1"/>
<field name="bank_statement_filename" invisible="1"/>
<field name="currency_name" invisible="1" />
<field name="country_code" invisible="1"/>
<sheet>
<div class="alert alert-warning" role="alert"
attrs="{'invisible': [('kyc_status_message', '=', False)]}">
<field name="kyc_status_message"/>
</div>
<group>
<group>
<field name="country_id" options='{"no_open": True, "no_create": True}'/>
<field name="currency_id" options="{'no_open': True, 'no_create': True}"/>
<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_city"
attrs="{'invisible': [('currency_name', '!=', 'USD')], 'required': [('currency_name', '=', 'USD')]}"/>
<field name="bank_code"
attrs="{'invisible': [('country_code', '!=', 'CA')], 'required': [('country_code', '=', 'CA')]}"/>
<field name="bank_name"
attrs="{'invisible': [('currency_name', '!=', 'USD')], 'required': [('currency_name', '=', 'USD')]}"/>
<field name="bank_statement" filename="bank_statement_filename" attrs="{'invisible': [('bank_account_uuid', '=', False)]}"/>
</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': [('currency_name', '!=', 'USD')]}">
<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': [('currency_name', '=', 'USD')]}"/>
<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>
</odoo>
@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<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">
<field name="adyen_kyc_ids" invisible="1" />
<field name="shareholder_uuid" invisible="1"/>
<field name="shareholder_reference" invisible="1" />
<sheet>
<field name="kyc_status" />
<div class="alert alert-warning" role="alert" attrs="{'invisible': [('kyc_status_message', '=', False)]}">
<field name="kyc_status_message"/>
</div>
<group>
<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>
</odoo>
@@ -1,150 +0,0 @@
<?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>
<field name="fees_currency_id" invisible="1" />
<field name="currency_id" invisible="1"/>
<field name="signature" invisible="1"/>
<header>
<button string="Refund" type="object" name="action_refund" attrs="{'invisible': ['|', ('total_amount', '&lt;=', 0), ('signature', '=', False)]}" />
</header>
<sheet>
<div class="oe_button_box" name="button_box" />
<group>
<group>
<field name="date"/>
<field name="description"/>
<field name="shopper_country_id"/>
<field name="commercial_card"/>
</group>
<group>
<field name="reference"/>
<field name="status"/>
<field name="total_amount" widget='monetary'/>
<field name="merchant_amount" widget='monetary'/>
<field name="fixed_fees" widget="monetary" options="{'currency_field': 'fees_currency_id'}" groups="base.group_no_one" />
<field name="variable_fees" widget="monetary" options="{'currency_field': 'fees_currency_id'}" groups="base.group_no_one" />
<field name="fees" widget="monetary" options="{'currency_field': 'fees_currency_id'}" />
<field name="reason" attrs="{'invisible': [('reason', '=', False)]}" />
</group>
</group>
<group>
<group name="additional_info">
</group>
<group>
<field name="status_ids" nolabel="1">
<tree>
<field name="status" />
<field name="date" />
</tree>
</field>
</group>
</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 decoration-danger="reason != False">
<field name="adyen_account_id" invisible="1" />
<field name="reason" invisible="1" />
<field name="currency_id" invisible="1" />
<field name="fees_currency_id" invisible="1" />
<field name="description"/>
<field name="reference" optional="show" />
<field name="payment_method" optional="show" />
<field name="shopper_country_id" optional="show" />
<field name="card_country_id" optional="show" />
<field name="commercial_card" optional="hide" />
<field name="total_amount" widget="monetary" />
<field name="merchant_amount" widget="monetary" />
<field name="fixed_fees" widget="monetary" options="{'currency_field': 'fees_currency_id'}" groups="base.group_no_one" optional="hide" />
<field name="variable_fees" widget="monetary" options="{'currency_field': 'fees_currency_id'}" groups="base.group_no_one" optional="hide" />
<field name="fees" widget="monetary" options="{'currency_field': 'fees_currency_id'}" optional="show" />
<field name="date"/>
<field name="status"/>
</tree>
</field>
</record>
<record id="adyen_transaction_view_search" model="ir.ui.view">
<field name="model">adyen.transaction</field>
<field name="arch" type="xml">
<search string="Transactions">
<field name="description" />
<field name="reference" />
<field name="status" />
<filter name="filter_date" date="date" string="Transaction Date" />
<filter name="filter_last_status_update" date="date" string="Last Status Update" />
<group>
<filter string="Currency" name="groupby_currency_id" context="{'group_by': 'currency_id'}"/>
<filter string="Date" name="groupby_date" context="{'group_by': 'date'}" />
</group>
</search>
</field>
</record>
<record id="adyen_transaction_payout_view_list" model="ir.ui.view">
<field name="name">adyen.transaction.payout.view.list</field>
<field name="model">adyen.transaction.payout</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree js_class="adyen_transactions_list" expand="1">
<field name="currency_id" invisible="1" />
<field name="reference" optional="show" />
<field name="bank_account_id" />
<field name="amount" string="Amount" widget="monetary" />
<field name="date"/>
<field name="status"/>
</tree>
</field>
</record>
<record id="adyen_transaction_payout_view_search" model="ir.ui.view">
<field name="model">adyen.transaction.payout</field>
<field name="arch" type="xml">
<search string="Payouts">
<field name="reference" />
<field name="status" />
<filter name="filter_date" date="date" string="Transaction Date" />
<group>
<filter string="Currency" name="groupby_currency_id" context="{'group_by': 'currency_id'}"/>
<filter string="Date" name="groupby_date" context="{'group_by': 'date'}" />
</group>
</search>
</field>
</record>
<record id="adyen_transaction_action" model="ir.actions.act_window">
<field name="name">Payment Transactions</field>
<field name="res_model">adyen.transaction</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
There are no transactions yet.
</p>
</field>
</record>
<record id="adyen_balance_action" model="ir.actions.act_window">
<field name="name">Balance</field>
<field name="res_model">adyen.transaction.payout</field>
<field name="view_mode">tree</field>
<field name="domain">[]</field>
<field name="context">{}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No payouts yet.
</p>
</field>
</record>
</odoo>
@@ -148,38 +148,6 @@
</record>
<record id="payment_acquirer_odoo" model="payment.acquirer">
<field name="name">Odoo Payments</field>
<field name="sequence">10</field>
<field name="display_as">Credit Card (powered by Odoo Payments)</field>
<field name="image_128" type="base64" file="payment_odoo/static/src/img/odoo_icon.png"/>
<field name="module_id" ref="base.module_payment_odoo"/>
<field name="description" type="html">
<p>
A payment gateway to accept online payments via credit cards, debit cards and bank
transfers.
</p>
<ul class="list-inline">
<li class="list-inline-item"><i class="fa fa-check"/>Online Payment</li>
<li class="list-inline-item"><i class="fa fa-check"/>Subscriptions</li>
<li class="list-inline-item"><i class="fa fa-check"/>Save Cards</li>
<li class="list-inline-item"><i class="fa fa-check"/>Payment Status Tracking</li>
</ul>
</field>
<!-- https://www.adyen.com/payment-methods -->
<field name="payment_icon_ids"
eval="[(6, 0, [
ref('payment.payment_icon_cc_bancontact'),
ref('payment.payment_icon_cc_maestro'),
ref('payment.payment_icon_cc_mastercard'),
ref('payment.payment_icon_cc_visa'),
ref('payment.payment_icon_cc_discover'),
ref('payment.payment_icon_cc_diners_club_intl'),
ref('payment.payment_icon_cc_jcb'),
ref('payment.payment_icon_cc_unionpay'),
])]"/>
</record>
<record id="payment_acquirer_ogone" model="payment.acquirer">
<field name="name">Ogone</field>
<field name="display_as">Credit Card (powered by Ogone)</field>
-10
View File
@@ -1,10 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from odoo.addons.payment import reset_payment_acquirer
def uninstall_hook(cr, registry):
reset_payment_acquirer(cr, registry, 'odoo')
-20
View File
@@ -1,20 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Odoo Payments Payment Acquirer',
'version': '2.0',
'category': 'Accounting/Payment Acquirers',
'sequence': 330,
'summary': 'Payment Acquirer: Odoo Payments',
'description': """Odoo Payments""",
'depends': ['payment', 'adyen_platforms'],
'data': [
'views/adyen_transaction_views.xml',
'views/payment_views.xml',
'views/payment_odoo_templates.xml',
'data/payment_acquirer_data.xml',
],
'application': True,
'uninstall_hook': 'uninstall_hook',
'license': 'LGPL-3',
}
-22
View File
@@ -1,22 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# Adyen-specific mapping of currency codes in ISO 4217 format to the number of decimals.
# Only currencies for which Adyen does not follow the ISO 4217 norm are listed here.
# See https://docs.adyen.com/development-resources/currency-codes
CURRENCY_DECIMALS = {
'CLP': 2,
'CVE': 0,
'IDR': 0,
'ISK': 2,
}
# Mapping of transaction states to Adyen result codes.
# See https://docs.adyen.com/checkout/payment-result-codes for the exhaustive list of result codes.
RESULT_CODES_MAPPING = {
'pending': (
'ChallengeShopper', 'IdentifyShopper', 'Pending', 'PresentToShopper', 'Received',
'RedirectShopper'
),
'done': ('Authorised',),
'cancel': ('Cancelled',),
}
@@ -1,3 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
-93
View File
@@ -1,93 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
import pprint
from odoo import http
from odoo.http import request
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment_odoo.const import CURRENCY_DECIMALS
_logger = logging.getLogger(__name__)
class OdooController(http.Controller):
_notification_url = '/payment/odoo/notification'
@http.route(_notification_url, type='json', auth='public')
def odoo_notification(self):
""" Process the data sent by Adyen to the webhook based on the event code.
See https://docs.adyen.com/development-resources/webhooks/understand-notifications for the
exhaustive list of event codes.
:return: None
"""
# Payload data represent a single notification's data. Because two notifications of a same
# batch can be related to different sub-merchants, the proxy splits the batches and send
# individual notifications one by one to this endpoint.
notification_data = json.loads(request.httprequest.data)
# Check the source and integrity of the notification
received_signature = notification_data.get('additionalData', {}).get(
'metadata.merchant_signature'
)
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data(
'odoo', notification_data
)
# Ignore signature for REFUND, the signature will be the one of the original transaction
event_code = notification_data['eventCode']
if event_code != 'REFUND' and not self._verify_notification_signature(
received_signature, tx_sudo
):
return
_logger.info("notification received:\n%s", pprint.pformat(notification_data))
if notification_data['success'] != 'true' and event_code != 'REFUND':
return # Don't handle failed events
request.env['adyen.transaction'].sudo()._handle_payment_notification(
notification_data, tx_sudo
)
# Reshape the notification data for parsing
if event_code == 'AUTHORISATION':
notification_data['resultCode'] = 'Authorised'
elif event_code == 'CANCELLATION':
notification_data['resultCode'] = 'Cancelled'
elif event_code in ['NOTIFICATION_OF_CHARGEBACK', 'CHARGEBACK']:
notification_data['resultCode'] = 'Chargeback'
elif event_code == 'REFUND':
notification_data['resultCode'] = 'Refund'
else:
return # Don't handle unsupported event codes
# Handle the notification data as a regular feedback
request.env['payment.transaction'].sudo()._handle_feedback_data('odoo', notification_data)
def _verify_notification_signature(self, received_signature, tx):
""" Check that the signature computed from the transaction values matches the received one.
:param str received_signature: The signature sent with the notification
:param recordset tx: The transaction of the notification, as a `payment.transaction` record
:return: Whether the signatures match
:rtype: str
"""
if not received_signature:
_logger.warning("ignored notification with missing signature")
return False
converted_amount = payment_utils.to_minor_currency_units(
tx.amount, tx.currency_id, CURRENCY_DECIMALS.get(tx.currency_id.name)
)
if not payment_utils.check_access_token(
received_signature, converted_amount, tx.currency_id.name, tx.reference
):
_logger.warning("ignored notification with invalid signature")
return False
return True
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="payment.payment_acquirer_odoo" model="payment.acquirer">
<field name="provider">odoo</field>
<field name="redirect_form_view_id" ref="redirect_form"/>
<field name="support_authorization">False</field>
<field name="support_fees_computation">False</field>
<field name="support_refund"></field>
<field name="support_tokenization">True</field>
<field name="allow_tokenization">True</field>
</record>
<record id="payment_method_odoo" model="account.payment.method">
<field name="name">Odoo</field>
<field name="code">odoo</field>
<field name="payment_type">inbound</field>
</record>
</odoo>
-215
View File
@@ -1,215 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * payment_odoo
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~14.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-07-12 07:49+0000\n"
"PO-Revision-Date: 2021-07-12 07:49+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: payment_odoo
#: model:ir.ui.menu,name:payment_odoo.menu_adyen_account
msgid "Account Configuration"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__partner_address
#: model_terms:ir.ui.view,arch_db:payment_odoo.adyen_transaction_view_form
msgid "Address"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_payment_acquirer__odoo_adyen_account_id
msgid "Adyen Account"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_account_bank_statement_line__adyen_transaction_ids
#: model:ir.model.fields,field_description:payment_odoo.field_account_move__adyen_transaction_ids
#: model:ir.model.fields,field_description:payment_odoo.field_account_payment__adyen_transaction_ids
#: model:ir.model.fields,field_description:payment_odoo.field_payment_transaction__adyen_transaction_ids
msgid "Adyen Transaction"
msgstr ""
#. module: payment_odoo
#: model:ir.model,name:payment_odoo.model_adyen_account
msgid "Adyen for Platforms Account"
msgstr ""
#. module: payment_odoo
#: model:ir.model,name:payment_odoo.model_adyen_transaction
msgid "Adyen for Platforms Transaction"
msgstr ""
#. module: payment_odoo
#: model:ir.ui.menu,name:payment_odoo.menu_adyen_balance
msgid "Balance"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__partner_city
#: model_terms:ir.ui.view,arch_db:payment_odoo.adyen_transaction_view_form
msgid "City"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__partner_country_id
#: model_terms:ir.ui.view,arch_db:payment_odoo.adyen_transaction_view_form
msgid "Country"
msgstr ""
#. module: payment_odoo
#: model_terms:ir.ui.view,arch_db:payment_odoo.payment_acquirer_form
msgid "Create an account in 1 minute"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__partner_id
msgid "Customer"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__partner_email
msgid "Email"
msgstr ""
#. module: payment_odoo
#: model_terms:ir.ui.view,arch_db:payment_odoo.adyen_transaction_view_form
msgid "Invoice(s)"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__invoice_ids
msgid "Invoices"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__invoices_count
msgid "Invoices Count"
msgstr ""
#. module: payment_odoo
#: model:ir.model,name:payment_odoo.model_account_move
msgid "Journal Entry"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__partner_lang
msgid "Language"
msgstr ""
#. module: payment_odoo
#: code:addons/payment_odoo/models/payment_transaction.py:0
#, python-format
msgid "No transaction found matching reference %s."
msgstr ""
#. module: payment_odoo
#: model:account.payment.method,name:payment_odoo.payment_method_odoo
msgid "Odoo"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields.selection,name:payment_odoo.selection__payment_acquirer__provider__odoo
#: model:ir.ui.menu,name:payment_odoo.root_adyen_menu
msgid "Odoo Payments"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__partner_name
msgid "Partner Name"
msgstr ""
#. module: payment_odoo
#: model:ir.model,name:payment_odoo.model_payment_acquirer
msgid "Payment Acquirer"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_payment_token__odoo_payment_method_type
msgid "Payment Method Type"
msgstr ""
#. module: payment_odoo
#: model:ir.model,name:payment_odoo.model_account_payment_method
msgid "Payment Methods"
msgstr ""
#. module: payment_odoo
#: model:ir.model,name:payment_odoo.model_payment_token
msgid "Payment Token"
msgstr ""
#. module: payment_odoo
#: model:ir.model,name:payment_odoo.model_payment_transaction
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__payment_transaction_id
msgid "Payment Transaction"
msgstr ""
#. module: payment_odoo
#: model:ir.ui.menu,name:payment_odoo.menu_adyen_transaction
msgid "Payment Transactions"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__partner_phone
msgid "Phone"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_payment_acquirer__provider
msgid "Provider"
msgstr ""
#. module: payment_odoo
#: code:addons/payment_odoo/models/payment_transaction.py:0
#, python-format
msgid "Received data with invalid payment state: %s"
msgstr ""
#. module: payment_odoo
#: code:addons/payment_odoo/models/payment_transaction.py:0
#, python-format
msgid "Received data with missing merchant reference"
msgstr ""
#. module: payment_odoo
#: code:addons/payment_odoo/models/payment_transaction.py:0
#, python-format
msgid "Received data with missing payment state."
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__partner_state_id
#: model_terms:ir.ui.view,arch_db:payment_odoo.adyen_transaction_view_form
msgid "State"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,help:payment_odoo.field_payment_acquirer__provider
msgid "The Payment Service Provider to use with this acquirer"
msgstr ""
#. module: payment_odoo
#: code:addons/payment_odoo/models/payment_transaction.py:0
#, python-format
msgid "The transaction is not linked to a token."
msgstr ""
#. module: payment_odoo
#: model_terms:ir.ui.view,arch_db:payment_odoo.adyen_transaction_view_form
msgid "ZIP"
msgstr ""
#. module: payment_odoo
#: model:ir.model.fields,field_description:payment_odoo.field_adyen_transaction__partner_zip
msgid "Zip"
msgstr ""
@@ -1,124 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * payment_odoo_by_adyen
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-27 14:12+0000\n"
"PO-Revision-Date: 2020-11-27 14:12+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: payment_odoo_by_adyen
#: code:addons/payment_odoo_by_adyen/models/payment.py:0
#, python-format
msgid "; multiple order found"
msgstr ""
#. module: payment_odoo_by_adyen
#: code:addons/payment_odoo_by_adyen/models/payment.py:0
#, python-format
msgid "; no order found"
msgstr ""
#. module: payment_odoo_by_adyen
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_acquirer__odoo_adyen_account_id
msgid "Adyen Account"
msgstr ""
#. module: payment_odoo_by_adyen
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_acquirer__odoo_adyen_payout_id
msgid "Adyen Payout"
msgstr ""
#. module: payment_odoo_by_adyen
#: code:addons/payment_odoo_by_adyen/models/payment.py:0
#, python-format
msgid "Card No XXXXXXXXXXXX%s"
msgstr ""
#. module: payment_odoo_by_adyen
#: model_terms:ir.ui.view,arch_db:payment_odoo_by_adyen.acquirer_form_odoo_by_adyen
msgid "Create an account in 1 minute"
msgstr ""
#. module: payment_odoo_by_adyen
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_acquirer__display_name
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_token__display_name
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_transaction__display_name
msgid "Display Name"
msgstr ""
#. module: payment_odoo_by_adyen
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_acquirer__id
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_token__id
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_transaction__id
msgid "ID"
msgstr ""
#. module: payment_odoo_by_adyen
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_acquirer____last_update
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_token____last_update
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_transaction____last_update
msgid "Last Modified on"
msgstr ""
#. module: payment_odoo_by_adyen
#: code:addons/payment_odoo_by_adyen/models/payment.py:0
#, python-format
msgid "Odoo Payment by Adyen: feedback error"
msgstr ""
#. module: payment_odoo_by_adyen
#: model:ir.model.fields.selection,name:payment_odoo_by_adyen.selection__payment_acquirer__provider__odoo_adyen
msgid "Odoo Payments by Adyen"
msgstr ""
#. module: payment_odoo_by_adyen
#: code:addons/payment_odoo_by_adyen/models/payment.py:0
#, python-format
msgid "Odoo Payments by Adyen is not available in test mode."
msgstr ""
#. module: payment_odoo_by_adyen
#: code:addons/payment_odoo_by_adyen/models/payment.py:0
#, python-format
msgid "Odoo Payments by Adyen: received data for reference %s"
msgstr ""
#. module: payment_odoo_by_adyen
#: code:addons/payment_odoo_by_adyen/models/payment.py:0
#, python-format
msgid "Odoo Payments by Adyen: received data with missing reference (%s)"
msgstr ""
#. module: payment_odoo_by_adyen
#: model:ir.model,name:payment_odoo_by_adyen.model_payment_acquirer
msgid "Payment Acquirer"
msgstr ""
#. module: payment_odoo_by_adyen
#: model:ir.model,name:payment_odoo_by_adyen.model_payment_token
msgid "Payment Token"
msgstr ""
#. module: payment_odoo_by_adyen
#: model:ir.model,name:payment_odoo_by_adyen.model_payment_transaction
msgid "Payment Transaction"
msgstr ""
#. module: payment_odoo_by_adyen
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_token__odoo_adyen_payment_method_type
msgid "PaymentMethod Type"
msgstr ""
#. module: payment_odoo_by_adyen
#: model:ir.model.fields,field_description:payment_odoo_by_adyen.field_payment_acquirer__provider
msgid "Provider"
msgstr ""
-9
View File
@@ -1,9 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move
from . import adyen_account
from . import adyen_transaction
from . import account_payment_method
from . import payment_acquirer
from . import payment_token
from . import payment_transaction
@@ -1,23 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class AccountMove(models.Model):
_inherit = 'account.move'
adyen_transaction_ids = fields.One2many(related='transaction_ids.adyen_transaction_ids')
def action_view_payment_transactions(self):
if any(provider != 'odoo' for provider in self.transaction_ids.mapped('provider')):
return super().action_view_payment_transactions()
action = self.env['ir.actions.act_window']._for_xml_id('adyen_platforms.adyen_transaction_action')
if len(self.adyen_transaction_ids) == 1:
action['view_mode'] = 'form'
action['res_id'] = self.adyen_transaction_ids.id
action['views'] = []
else:
action['domain'] = [('id', 'in', self.adyen_transaction_ids.ids)]
return action
@@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class AccountPaymentMethod(models.Model):
_inherit = 'account.payment.method'
@api.model
def _get_payment_method_information(self):
res = super()._get_payment_method_information()
res['odoo'] = {'mode': 'unique', 'domain': [('type', '=', 'bank')]}
return res
@@ -1,12 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AdyenAccount(models.Model):
_inherit = 'adyen.account'
def unlink(self):
acquirer = self.env['payment.acquirer'].search([('provider', '=', 'odoo'), ('odoo_adyen_account_id', 'in', self.ids)])
acquirer.state = 'disabled'
return super().unlink()
@@ -1,57 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.tools.float_utils import float_is_zero
class AdyenTransaction(models.Model):
_inherit = 'adyen.transaction'
payment_transaction_id = fields.Many2one(comodel_name='payment.transaction')
# payment.transaction fields
invoice_ids = fields.Many2many(related='payment_transaction_id.invoice_ids')
invoices_count = fields.Integer(related='payment_transaction_id.invoices_count')
partner_id = fields.Many2one(related='payment_transaction_id.partner_id')
partner_name = fields.Char(related='payment_transaction_id.partner_name')
partner_lang = fields.Selection(related='payment_transaction_id.partner_lang')
partner_email = fields.Char(related='payment_transaction_id.partner_email')
partner_address = fields.Char(related='payment_transaction_id.partner_address')
partner_zip = fields.Char(related='payment_transaction_id.partner_zip')
partner_city = fields.Char(related='payment_transaction_id.partner_city')
partner_state_id = fields.Many2one(related='payment_transaction_id.partner_state_id')
partner_country_id = fields.Many2one(related='payment_transaction_id.partner_country_id')
partner_phone = fields.Char(related='payment_transaction_id.partner_phone')
@api.model
def _handle_payment_notification(self, notification_data, payment_tx):
tx_sudo = self._handle_notification(notification_data)
if not tx_sudo.payment_transaction_id:
tx_sudo.payment_transaction_id = payment_tx.id
# Update fees on payment.transaction
if float_is_zero(payment_tx.fees, precision_digits=payment_tx.currency_id.decimal_places) and not float_is_zero(tx_sudo.fees, precision_digits=tx_sudo.fees_currency_id.decimal_places):
amount = tx_sudo.fees_currency_id._convert(tx_sudo.fees, payment_tx.currency_id, self.env.company, tx_sudo.date or fields.Date.today())
payment_tx.fees = amount
def _create_missing_tx(self, account_id, transaction, **kwargs):
tx = super()._create_missing_tx(account_id, transaction, **kwargs)
if tx.description and not tx.payment_transaction_id:
payment_tx = self.env['payment.transaction'].search([('reference', '=', tx.description), ('provider', '=', 'odoo')])
if payment_tx:
tx.payment_transaction_id = payment_tx.id
if not transaction.get('disputePspReference') and tx.merchant_amount > 0:
tx.total_amount = payment_tx.amount
tx.currency_id = payment_tx.currency_id
fees_amount = tx.total_amount - tx.merchant_amount
tx.fees = payment_tx.currency_id._convert(fees_amount, tx.fees_currency_id, self.env.company, tx.date or fields.Date.today())
tx.variable_fees = tx.fees
return tx
def action_view_invoices(self):
self.ensure_one()
return self.payment_transaction_id.action_view_invoices()
@@ -1,40 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug import urls
from odoo import fields, models
class PaymentAcquirer(models.Model):
_inherit = 'payment.acquirer'
provider = fields.Selection(
selection_add=[('odoo', "Odoo Payments")], ondelete={'odoo': 'set default'})
odoo_adyen_account_id = fields.Many2one(
related='company_id.adyen_account_id', required_if_provider='odoo')
def odoo_create_adyen_account(self):
return self.env['adyen.account'].action_create_redirect()
def _odoo_get_api_url(self):
self.ensure_one()
proxy_url = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.proxy_url')
url = 'v1/pay_by_link' if self.state == 'enabled' else 'v1/test_pay_by_link'
return urls.url_join(proxy_url, url)
def _odoo_compute_shopper_reference(self, partner_id):
""" Compute a unique reference of the partner for Adyen.
This is used for the `shopperReference` field in communications with Adyen.
:param recordset partner_id: The partner making the transaction, as a `res.partner` id
:return: The unique reference for the partner
:rtype: str
"""
return f'{self.odoo_adyen_account_id.adyen_uuid}_{partner_id}'
def _get_default_payment_method_id(self):
self.ensure_one()
if self.provider != 'odoo':
return super()._get_default_payment_method_id()
return self.env.ref('payment_odoo.payment_method_odoo').id
@@ -1,9 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class PaymentToken(models.Model):
_inherit = 'payment.token'
odoo_payment_method_type = fields.Char(string="Payment Method Type")
@@ -1,236 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
import pprint
from werkzeug import urls
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment_odoo.const import CURRENCY_DECIMALS, RESULT_CODES_MAPPING
from odoo.addons.payment_odoo.controllers.main import OdooController
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
adyen_transaction_ids = fields.One2many('adyen.transaction', 'payment_transaction_id')
def _get_specific_rendering_values(self, processing_values):
""" Override of payment to return Odoo-specific rendering values.
Note: self.ensure_one() from `_get_processing_values`
:param dict processing_values: The generic and specific processing values of the transaction
:return: The dict of acquirer-specific processing values
:rtype: dict
"""
res = super()._get_specific_rendering_values(processing_values)
if self.provider != 'odoo':
return res
converted_amount = payment_utils.to_minor_currency_units(
self.amount, self.currency_id, CURRENCY_DECIMALS.get(self.currency_id.name)
)
# The lang is taken from the context rather than from the partner because it is not required
# to be logged to make a payment and because the lang is not always set on the partner.
# Adyen only supports a reduced set of languages but, instead of looking for the closest
# match in https://docs.adyen.com/checkout/components-web/localization-components, we simply
# provide the lang string as is (after adapting the format) and let Adyen find the best fit.
lang_code = (self._context.get('lang') or 'en-US').replace('_', '-')
base_url = self.acquirer_id.get_base_url()
signature = payment_utils.generate_access_token(
converted_amount, self.currency_id.name, self.reference
)
data = {
'amount': {
'value': converted_amount,
'currency': self.currency_id.name,
},
'reference': self.reference,
'shopperLocale': lang_code,
'shopperReference': self.acquirer_id._odoo_compute_shopper_reference(
self.partner_id.id
) if self.tokenize else '',
'recurringProcessingModel': 'Subscription' if self.tokenize else '',
'storePaymentMethod': self.tokenize, # True by default on Adyen side
# Since the Pay by Link API redirects the customer without any payload, we use the
# /payment/status route directly as return url.
'returnUrl': urls.url_join(base_url, '/payment/status'),
'metadata': {
'merchant_signature': signature,
'notification_url': urls.url_join(base_url, OdooController._notification_url),
'adyen_uuid': self.acquirer_id.odoo_adyen_account_id.adyen_uuid,
'payout': self.acquirer_id.odoo_adyen_account_id.account_code,
}, # Proxy-specific data
}
return {
'data': json.dumps(data),
'api_url': self.acquirer_id._odoo_get_api_url(),
}
def _send_payment_request(self):
""" Override of payment to send a payment request to Adyen through the Odoo proxy.
Note: self.ensure_one()
:return: None
:raise: UserError if the transaction is not linked to a token
"""
super()._send_payment_request()
if self.provider != 'odoo':
return
# Make the payment request
if not self.token_id:
raise UserError("Odoo Payments: " + _("The transaction is not linked to a token."))
converted_amount = payment_utils.to_minor_currency_units(
self.amount, self.currency_id, CURRENCY_DECIMALS.get(self.currency_id.name)
)
base_url = self.acquirer_id.get_base_url()
signature = payment_utils.generate_access_token(
converted_amount, self.currency_id.name, self.reference
)
data = {
'amount': {
'value': converted_amount,
'currency': self.currency_id.name,
},
'reference': self.reference,
'paymentMethod': {
'type': self.token_id.odoo_payment_method_type,
'storedPaymentMethodId': self.token_id.acquirer_ref,
},
'shopperReference': self.acquirer_id._odoo_compute_shopper_reference(
self.partner_id.id
),
'recurringProcessingModel': 'Subscription',
'shopperInteraction': 'ContAuth',
'metadata': {
'merchant_signature': signature,
'notification_url': urls.url_join(base_url, OdooController._notification_url),
'adyen_uuid': self.acquirer_id.odoo_adyen_account_id.adyen_uuid,
'payout': self.acquirer_id.odoo_adyen_account_id.account_code,
}, # Proxy-specific data
}
response_content = self.acquirer_id.odoo_adyen_account_id._adyen_rpc('v1/payments', data)
# Handle the payment request response
_logger.info("payment request response:\n%s", pprint.pformat(response_content))
self._handle_feedback_data('odoo', response_content)
@api.model
def _get_tx_from_feedback_data(self, provider, data):
""" Override of payment to find the transaction based on Adyen data.
:param str provider: The provider of the acquirer that handled the transaction
:param dict data: The feedback data sent by the provider
:return: The transaction if found
:rtype: recordset of `payment.transaction`
:raise: ValidationError if inconsistent data were received
:raise: ValidationError if the data match no transaction
"""
tx = super()._get_tx_from_feedback_data(provider, data)
if provider != 'odoo':
return tx
reference = data.get('merchantReference') or data.get('additionalData', {}).get('merchantReference')
if not reference:
raise ValidationError(
"Odoo Payments: " + _("Received data with missing merchant reference")
)
tx = self.search([('reference', '=', reference), ('provider', '=', 'odoo')])
if not tx:
raise ValidationError(
"Odoo Payments: " + _("No transaction found matching reference %s.", reference)
)
return tx
def _process_feedback_data(self, data):
""" Override of payment to process the transaction based on Adyen data.
Since only webhook notifications send data, the parsing is exclusively done according to the
structure of NotificationRequestItem object.
See https://docs.adyen.com/development-resources/webhooks/notifications-api
Note: self.ensure_one()
:param dict data: The feedback data sent by the provider
:return: None
:raise: ValidationError if inconsistent data were received
"""
super()._process_feedback_data(data)
if self.provider != 'odoo':
return
# Handle the acquirer reference
if 'originalReference' in data:
self.acquirer_reference = data['originalReference']
elif 'pspReference' in data:
self.acquirer_reference = data['pspReference']
# Handle the payment state
payment_state = data.get('resultCode')
if not payment_state:
raise ValidationError(
"Odoo Payments: " + _("Received data with missing payment state.")
)
if payment_state in RESULT_CODES_MAPPING['pending']:
self._set_pending()
elif payment_state in RESULT_CODES_MAPPING['done']:
has_token_data = 'recurring.recurringDetailReference' in data.get('additionalData', {})
if self.tokenize and has_token_data:
self._odoo_tokenize_from_feedback_data(data)
self._set_done()
elif payment_state in RESULT_CODES_MAPPING['cancel']:
self._set_canceled()
else: # Classify unsupported payment state as `error` tx state
_logger.info("received data with invalid payment state: %s", payment_state)
self._set_error(
"Odoo Payments: " + _("Received data with invalid payment state: %s", payment_state)
)
def _odoo_tokenize_from_feedback_data(self, data):
""" Create a new token based on the feedback data.
Note: self.ensure_one()
:param dict data: The feedback data sent by the provider
:return: None
"""
self.ensure_one()
# Retrieve all stored payment methods for the customer from the API and match them with the
# acquirer reference of the transaction to find its payment method
response_content = self.acquirer_id.odoo_adyen_account_id._adyen_rpc(
'v1/payment_methods',
dict(shopperReference=data['additionalData']['recurring.shopperReference']),
)
payment_methods = response_content['storedPaymentMethods']
acquirer_ref = data['additionalData']['recurring.recurringDetailReference']
payment_method_type = next(pm['type'] for pm in payment_methods if pm['id'] == acquirer_ref)
# Create the token
token = self.env['payment.token'].create({
'acquirer_id': self.acquirer_id.id,
'name': payment_utils.build_token_name(data['additionalData'].get('cardSummary')),
'partner_id': self.partner_id.id,
'acquirer_ref': acquirer_ref,
'verified': True, # The payment is authorized, so the payment method is valid
'odoo_payment_method_type': payment_method_type,
})
self.write({
'token_id': token,
'tokenize': False,
})
_logger.info(
"created token with id %s for partner with id %s", token.id, self.partner_id.id
)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

@@ -1,39 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 70 70">
<defs>
<mask id="mask" x="0" y="0" width="70" height="70" maskUnits="userSpaceOnUse">
<g id="b">
<path id="a" d="M4,0H65c4,0,5,1,5,5V65c0,4-1,5-5,5H4c-3,0-4-1-4-5V5C0,1,1,0,4,0Z" fill="#fff" fill-rule="evenodd"/>
</g>
</mask>
<linearGradient id="linear-gradient" x1="-1261.07" y1="-4.39" x2="-1262.07" y2="-5.39" gradientTransform="matrix(70, 0, 0, -70, 88344.99, -307.17)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#7cc098"/>
<stop offset="1" stop-color="#5f8a71"/>
</linearGradient>
</defs>
<g mask="url(#mask)">
<g>
<path d="M0,0H70V70H0Z" fill-rule="evenodd" fill="url(#linear-gradient)"/>
<path d="M4,1H65c2.67,0,4.33.67,5,2V0H0V3C.67,1.67,2,1,4,1Z" fill="#fff" fill-opacity="0.38" fill-rule="evenodd"/>
<path d="M4,69a3.66,3.66,0,0,1-4-4V33.59L14.65,19l20.56,7.79,3.61-3.61L51.58,25l.17,16.3-2.21,2.21,2.32,2.81,1.93-1.93C59,41.79,61.72,47.93,59.06,50L40,69Z" fill="#393939" fill-rule="evenodd" opacity="0.32" style="isolation: isolate"/>
<path d="M4,69H65c2.67,0,4.33-1,5-3v4H0V66A3.92,3.92,0,0,0,4,69Z" fill-opacity="0.38" fill-rule="evenodd"/>
<g>
<g opacity="0.4">
<path d="M16.28,41.43h17v-17A4.26,4.26,0,0,0,29,20.15H12.36V25.5H23.3a1.18,1.18,0,0,1,1.18,1.19v9.39H22.11a1.19,1.19,0,0,1-1.19-1.19V28.11H16.25A4.27,4.27,0,0,0,12,32.39v4.8A4.26,4.26,0,0,0,16.28,41.43Z"/>
<path d="M47,42.15a7.21,7.21,0,0,1,2.71,1.12V27.45A2.73,2.73,0,0,0,47,24.7H36.5v2.75H46.7a.34.34,0,0,1,.34.34V31H36.5v6.5H47Z"/>
<path d="M20.87,49.25a7.4,7.4,0,0,1,.21-1.7H20.3a.34.34,0,0,1-.34-.34V45H17.25v2.55A2.73,2.73,0,0,0,20,50.3h1A7.2,7.2,0,0,1,20.87,49.25Z"/>
<polygon points="17.31 57.39 17.3 57.39 16.49 55 15.42 55 15.42 58.47 16.13 58.47 16.13 56.04 16.14 56.04 16.99 58.47 17.58 58.47 18.42 56.01 18.43 56.01 18.43 58.47 19.14 58.47 19.14 55 18.07 55 17.31 57.39"/>
<polygon points="12.42 55.64 13.46 55.64 13.46 58.47 14.22 58.47 14.22 55.64 15.26 55.64 15.26 55 12.42 55 12.42 55.64"/>
<path d="M54.58,53.47a4.22,4.22,0,1,1,4.22-4.22A4.22,4.22,0,0,1,54.58,53.47Zm0-1.74a2.48,2.48,0,1,0-2.47-2.48A2.48,2.48,0,0,0,54.58,51.73Zm-8.83,1.74A4.22,4.22,0,1,1,50,49.25,4.22,4.22,0,0,1,45.75,53.47Zm0-1.74a2.48,2.48,0,1,0-2.47-2.48A2.48,2.48,0,0,0,45.75,51.73ZM41.14,49.2a4.22,4.22,0,1,1-8.44,0,4.32,4.32,0,0,1,6.69-3.46V43.3a.88.88,0,0,1,1.75,0Zm-4.22,2.53a2.48,2.48,0,1,0-2.48-2.48A2.47,2.47,0,0,0,36.92,51.73Zm-8.83,1.74a4.22,4.22,0,1,1,4.22-4.22A4.22,4.22,0,0,1,28.09,53.47Zm0-1.74a2.48,2.48,0,1,0-2.48-2.48A2.47,2.47,0,0,0,28.09,51.73Z"/>
</g>
<g>
<path d="M18.28,39.43h17v-17A4.26,4.26,0,0,0,31,18.15H14.36V23.5H25.3a1.18,1.18,0,0,1,1.18,1.19v9.39H24.11a1.19,1.19,0,0,1-1.19-1.19V26.11H18.25A4.27,4.27,0,0,0,14,30.39v4.8A4.26,4.26,0,0,0,18.28,39.43Z" fill="#fff"/>
<path d="M49,40.15a7.21,7.21,0,0,1,2.71,1.12V25.45A2.73,2.73,0,0,0,49,22.7H38.5v2.75H48.7a.34.34,0,0,1,.34.34V29H38.5v6.5H49Z" fill="#fff"/>
<path d="M22.87,47.25a7.4,7.4,0,0,1,.21-1.7H22.3a.34.34,0,0,1-.34-.34V43H19.25v2.55A2.73,2.73,0,0,0,22,48.3h1A7.2,7.2,0,0,1,22.87,47.25Z" fill="#fff"/>
<polygon points="19.31 55.39 19.3 55.39 18.49 53 17.42 53 17.42 56.47 18.13 56.47 18.13 54.04 18.14 54.04 18.99 56.47 19.58 56.47 20.42 54.01 20.43 54.01 20.43 56.47 21.14 56.47 21.14 53 20.07 53 19.31 55.39" fill="#fff"/>
<polygon points="14.42 53.64 15.46 53.64 15.46 56.47 16.22 56.47 16.22 53.64 17.26 53.64 17.26 53 14.42 53 14.42 53.64" fill="#fff"/>
<path d="M56.58,51.47a4.22,4.22,0,1,1,4.22-4.22A4.22,4.22,0,0,1,56.58,51.47Zm0-1.74a2.48,2.48,0,1,0-2.47-2.48A2.48,2.48,0,0,0,56.58,49.73Zm-8.83,1.74A4.22,4.22,0,1,1,52,47.25,4.22,4.22,0,0,1,47.75,51.47Zm0-1.74a2.48,2.48,0,1,0-2.47-2.48A2.48,2.48,0,0,0,47.75,49.73ZM43.14,47.2a4.22,4.22,0,1,1-8.44,0,4.32,4.32,0,0,1,6.69-3.46V41.3a.88.88,0,0,1,1.75,0Zm-4.22,2.53a2.48,2.48,0,1,0-2.48-2.48A2.47,2.47,0,0,0,38.92,49.73Zm-8.83,1.74a4.22,4.22,0,1,1,4.22-4.22A4.22,4.22,0,0,1,30.09,51.47Zm0-1.74a2.48,2.48,0,1,0-2.48-2.48A2.47,2.47,0,0,0,30.09,49.73Z" fill="#fff"/>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="adyen_transaction_view_list" model="ir.ui.view">
<field name="model">adyen.transaction</field>
<field name="inherit_id" ref="adyen_platforms.adyen_transaction_view_list"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='description']" position="after">
<field name="payment_transaction_id" optional="show" />
</xpath>
</field>
</record>
<record id="adyen_transaction_view_form" model="ir.ui.view">
<field name="model">adyen.transaction</field>
<field name="inherit_id" ref="adyen_platforms.adyen_transaction_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='description']" position="after">
<field name="payment_transaction_id" attrs="{'invisible': [('payment_transaction_id', '=', False)]}" />
</xpath>
<xpath expr="//group[@name='additional_info']" position="inside">
<field name="partner_id" widget="res_partner_many2one" attrs="{'invisible': [('partner_id', '=', False)]}"/>
<label for="partner_address" string="Address" attrs="{'invisible': [('partner_id', '=', False)]}"/>
<div class="o_address_format" attrs="{'invisible': [('partner_id', '=', False)]}">
<field name="partner_address" placeholder="Address" class="o_address_street"/>
<field name="partner_city" placeholder="City" class="o_address_city"/>
<field name="partner_state_id" placeholder="State" class="o_address_state" options="{'no_open': True}"/>
<field name="partner_zip" placeholder="ZIP" class="o_address_zip"/>
<field name="partner_country_id" placeholder="Country" class="o_address_country" options="{'no_open': True}"/>
</div>
<field name="partner_email" widget="email" attrs="{'invisible': [('partner_id', '=', False)]}"/>
<field name="partner_phone" widget="phone" attrs="{'invisible': [('partner_id', '=', False)]}"/>
<field name="partner_lang" attrs="{'invisible': [('partner_id', '=', False)]}"/>
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_view_invoices" type="object"
class="oe_stat_button" icon="fa-money"
attrs="{'invisible': [('invoices_count', '=', 0)]}">
<field name="invoices_count" widget="statinfo" string="Invoice(s)"/>
</button>
</xpath>
</field>
</record>
</odoo>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="redirect_form">
<form t-att-action="api_url" method="post">
<input type="hidden" name="data" t-att-value="data"/>
</form>
</template>
</odoo>
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_acquirer_form" model="ir.ui.view">
<field name="name">Odoo Payments Acquirer Form</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.payment_acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer"]' position='inside'>
<group attrs="{'invisible': [('provider', '!=', 'odoo')]}">
<button name="odoo_create_adyen_account"
icon="fa-arrow-right"
type="object"
class="btn-link"
string="Create an account in 1 minute"
attrs="{'invisible': [('odoo_adyen_account_id', '!=', False)]}"/>
<field name="odoo_adyen_account_id"
readonly="1"
attrs="{'invisible': [('odoo_adyen_account_id', '=', False)]}"/>
</group>
</xpath>
</field>
</record>
<menuitem id="root_adyen_menu" name="Odoo Payments" parent="account.root_payment_menu" sequence="10" />
<menuitem id="menu_adyen_transaction" parent="root_adyen_menu" action="adyen_platforms.adyen_transaction_action"/>
<menuitem id="menu_adyen_balance" parent="root_adyen_menu" action="adyen_platforms.adyen_balance_action"/>
<menuitem id="menu_adyen_account" parent="root_adyen_menu" action="adyen_platforms.adyen_account_action"/>
</odoo>
+1 -5
View File
@@ -8,14 +8,10 @@
'summary': 'Integrate your POS with an Adyen payment terminal',
'description': '',
'data': [
'security/ir.model.access.csv',
'views/adyen_account_views.xml',
'views/adyen_transaction_views.xml',
'views/pos_config_views.xml',
'views/pos_payment_method_views.xml',
'views/res_config_settings_views.xml',
],
'depends': ['adyen_platforms', 'point_of_sale'],
'depends': ['point_of_sale'],
'installable': True,
'assets': {
'point_of_sale.assets': [
-3
View File
@@ -1,8 +1,5 @@
# 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 pos_config
from . import pos_payment_method
from . import res_config_settings
-113
View File
@@ -1,113 +0,0 @@
# -*- 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_account_id = self.env['adyen.account'].browse(values.get('adyen_account_id'))
response = adyen_account_id._adyen_rpc('v1/create_store', self._format_data(values))
stores = response['accountHolderDetails']['storeDetails']
created_store = next(store for store in stores if store['storeReference'] == values.get('store_reference'))
values['store_uuid'] = created_store['store']
adyen_store_id = super(AdyenStore, self).create(values)
return adyen_store_id
def unlink(self):
for store_id in self:
store_id.adyen_account_id._adyen_rpc('v1/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, values):
adyen_account_id = self.env['adyen.account'].browse(values.get('adyen_account_id')) if values.get('adyen_account_id') else self.adyen_account_id
country_id = self.env['res.country'].browse(values.get('country_id')) if values.get('country_id') else self.country_id
state_id = self.env['res.country.state'].browse(values.get('state_id')) if values.get('state_id') else self.state_id
return {
'accountHolderCode': adyen_account_id.account_holder_code,
'accountHolderDetails': {
'storeDetails': [{
'storeReference': values.get('store_reference') or self.store_reference,
'storeName': values.get('name') or self.name,
'merchantCategoryCode': '7999',
'address': {
'city': values.get('city') or self.city,
'country': country_id.code,
'houseNumberOrName': values.get('house_number_or_name') or self.house_number_or_name,
'postalCode': values.get('zip') or self.zip,
'stateOrProvince': state_id.code or None,
'street': values.get('street') or self.street,
},
'fullPhoneNumber': values.get('phone_number') or 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('v1/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,
})
@@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class AdyenTransaction(models.Model):
_inherit = 'adyen.transaction'
pos_payment_id = fields.Many2one('pos.payment', string='POS Order')
pos_order_id = fields.Many2one(related='pos_payment_id.pos_order_id')
def _post_transaction_sync(self):
to_match = self.filtered(lambda t: not t.pos_payment_id)
pos_payment_ids = self.env['pos.payment'].search([('transaction_id', 'in', to_match.mapped('reference'))])
for tx in to_match:
tx.pos_payment_id = pos_payment_ids.filtered(lambda t: t.transaction_id == tx.reference)
+2 -30
View File
@@ -17,17 +17,13 @@ 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')]
return super(PosPaymentMethod, self)._get_payment_terminal_selection() + [('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_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')
@@ -48,12 +44,6 @@ class PosPaymentMethod(models.Model):
'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)
@@ -100,10 +90,7 @@ class PosPaymentMethod(models.Model):
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)
return self._proxy_adyen_request_direct(data, operation)
def _proxy_adyen_request_direct(self, data, operation):
self.ensure_one()
@@ -131,18 +118,3 @@ class PosPaymentMethod(models.Model):
return True
return req.json()
def _proxy_adyen_request_odoo_proxy(self, data, operation):
try:
company_sudo = self.env.company.sudo()
return company_sudo.adyen_account_id._adyen_rpc('v1/%s' % operation, {
'request_data': data,
'account_code': self.sudo().adyen_account_id.account_code,
'notification_url': company_sudo.get_base_url(),
})
except Forbidden:
return {
'error': {
'status_code': 401,
}
}
@@ -1,13 +0,0 @@
# -*- 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()
@@ -1,3 +0,0 @@
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_terminal_group_pos_manager,adyen.terminal,model_adyen_terminal,point_of_sale.group_pos_manager,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_adyen_store_group_erp_manager adyen.store model_adyen_store base.group_erp_manager 1 0 1 1
3 access_adyen_terminal_group_pos_manager adyen.terminal model_adyen_terminal point_of_sale.group_pos_manager 1 0 0 0
-1
View File
@@ -3,6 +3,5 @@ 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');
});
@@ -1,68 +0,0 @@
<?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_reference" invisible="1" />
<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>
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="adyen_transaction_view_form" model="ir.ui.view">
<field name="model">adyen.transaction</field>
<field name="inherit_id" ref="adyen_platforms.adyen_transaction_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='description']" position="after">
<field name="pos_order_id" attrs="{'invisible': [('pos_order_id', '=', False)]}" />
</xpath>
</field>
</record>
<record id="adyen_transaction_view_list" model="ir.ui.view">
<field name="model">adyen.transaction</field>
<field name="inherit_id" ref="adyen_platforms.adyen_transaction_view_list"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='description']" position="after">
<field name="pos_order_id" optional="show" />
</xpath>
</field>
</record>
</odoo>
@@ -10,10 +10,6 @@
<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')], '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')]}"/>
</xpath>
</field>
</record>
@@ -1,20 +0,0 @@
<?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>
-3
View File
@@ -1,3 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
-16
View File
@@ -1,16 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Sale Odoo Payments',
'version': '1.0',
'category': 'Accounting/Payment Acquirers',
'sequence': 330,
'summary': 'Payment Acquirer: Odoo Payments',
'description': """Sale Odoo Payments integration: show SO on payment transactions.""",
'depends': ['payment_odoo', 'sale'],
'data': [
'views/adyen_transaction_views.xml',
],
'auto_install': True,
'license': 'LGPL-3',
}
@@ -1,3 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import adyen_transaction
@@ -1,13 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class AdyenTransaction(models.Model):
_inherit = 'adyen.transaction'
sale_order_ids = fields.Many2many(related='payment_transaction_id.sale_order_ids')
sale_order_ids_nbr = fields.Integer(related='payment_transaction_id.sale_order_ids_nbr')
def action_view_sales_orders(self):
return self.payment_transaction_id.action_view_sales_orders()
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="adyen_transaction_view_form" model="ir.ui.view">
<field name="model">adyen.transaction</field>
<field name="inherit_id" ref="adyen_platforms.adyen_transaction_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_view_sales_orders" type="object"
class="oe_stat_button" icon="fa-money"
attrs="{'invisible': [('sale_order_ids_nbr', '=', 0)]}">
<field name="sale_order_ids_nbr" widget="statinfo" string="Sales Order(s)"/>
</button>
</xpath>
</field>
</record>
</odoo>