[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:
committed by
Antoine Vandevenne (anv)
co-authored by
Antoine Vandevenne (anv)
parent
7cc67aecec
commit
fbfd99e680
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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,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>
|
||||
@@ -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', '<=', 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>
|
||||
|
||||
@@ -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')
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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 ""
|
||||
@@ -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>
|
||||
@@ -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': [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user