Files
odoo_source/addons/adyen_platforms/models/adyen_account.py
T
Antoine Prieels f52c9fa1c0 [FIX] adyen_platforms: Sync KYC status on update
The response we get after updating data contains the new KYC status,
we then update it on the adyen account so that we don't have to wait
for the cron.

X-original-commit: 59fdb24527e6073cb3c4de5662d25d4d0cea08eb
2020-10-12 15:19:12 +00:00

723 lines
33 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import os
import requests
import uuid
from werkzeug.urls import url_join
from odoo import api, fields, models, _
from odoo.http import request
from odoo.exceptions import UserError, ValidationError
from odoo.tools import date_utils
from odoo.addons.adyen_platforms.util import AdyenProxyAuth
ADYEN_AVAILABLE_COUNTRIES = ['US', 'AT', 'AU', 'BE', 'CA', 'CH', 'CZ', 'DE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'IE', 'IT', 'LT', 'LU', 'NL', 'PL', 'PT']
TIMEOUT = 60
class AdyenAddressMixin(models.AbstractModel):
_name = 'adyen.address.mixin'
_description = 'Adyen for Plaforms Address Mixin'
country_id = fields.Many2one('res.country', string='Country', domain=[('code', 'in', ADYEN_AVAILABLE_COUNTRIES)], required=True)
country_code = fields.Char(related='country_id.code')
state_id = fields.Many2one('res.country.state', string='State', domain="[('country_id', '=?', country_id)]")
state_code = fields.Char(related='state_id.code')
city = fields.Char('City', required=True)
zip = fields.Char('ZIP', required=True)
street = fields.Char('Street', required=True)
house_number_or_name = fields.Char('House Number Or Name', required=True)
class AdyenIDMixin(models.AbstractModel):
_name = 'adyen.id.mixin'
_description = 'Adyen for Platforms ID Mixin'
id_type = fields.Selection(string='Photo ID type', selection=[
('PASSPORT', 'Passport'),
('ID_CARD', 'ID Card'),
('DRIVING_LICENSE', 'Driving License'),
])
id_front = fields.Binary('Photo ID Front', help="Allowed formats: jpg, pdf, png. Maximum allowed size: 4MB.")
id_front_filename = fields.Char()
id_back = fields.Binary('Photo ID Back', help="Allowed formats: jpg, pdf, png. Maximum allowed size: 4MB.")
id_back_filename = fields.Char()
def write(self, vals):
res = super(AdyenIDMixin, self).write(vals)
# Check file formats
if vals.get('id_front'):
self._check_file_requirements(vals.get('id_front'), vals.get('id_front_filename'))
if vals.get('id_back'):
self._check_file_requirements(vals.get('id_back'), vals.get('id_back_filename'))
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, filename):
file_extension = os.path.splitext(filename)[1]
file_size = int(len(content) * 3/4) # Compute file_size in bytes
if file_extension not in ['.jpeg', '.jpg', '.pdf', '.png']:
raise ValidationError(_('Allowed file formats for photo IDs are jpeg, jpg, pdf or png'))
if file_size >> 20 > 4 or (file_size >> 10 < 1 and file_extension == '.pdf') or (file_size >> 10 < 100 and file_extension != '.pdf') :
raise ValidationError(_('Photo ID file size must be between 100kB (1kB for PDFs) and 4MB'))
def _upload_photo_id(self, document_type, content, filename):
# The request to be sent to Adyen will be different for Individuals,
# Shareholders, etc. This method should be implemented by the models
# inheriting this mixin
raise NotImplementedError()
class AdyenAccount(models.Model):
_name = 'adyen.account'
_inherit = ['mail.thread', 'adyen.id.mixin', 'adyen.address.mixin']
_description = 'Adyen for Plaforms Account'
_rec_name = 'full_name'
# Credentials
proxy_token = fields.Char('Proxy Token')
adyen_uuid = fields.Char('Adyen UUID')
account_holder_code = fields.Char('Account Holder Code', default=lambda self: uuid.uuid4().hex)
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
payout_ids = fields.One2many('adyen.payout', 'adyen_account_id', string='Payouts')
shareholder_ids = fields.One2many('adyen.shareholder', 'adyen_account_id', string='Shareholders')
bank_account_ids = fields.One2many('adyen.bank.account', 'adyen_account_id', string='Bank Accounts')
transaction_ids = fields.One2many('adyen.transaction', 'adyen_account_id', string='Transactions')
transactions_count = fields.Integer(compute='_compute_transactions_count')
is_business = fields.Boolean('Is a business', required=True)
# Contact Info
full_name = fields.Char(compute='_compute_full_name')
email = fields.Char('Email', required=True)
phone_number = fields.Char('Phone Number', required=True)
# Individual
first_name = fields.Char('First Name')
last_name = fields.Char('Last Name')
date_of_birth = fields.Date('Date of birth')
document_number = fields.Char('ID Number',
help="The type of ID Number required depends on the country:\n"
"US: Social Security Number (9 digits or last 4 digits)\n"
"Canada: Social Insurance Number\nItaly: Codice fiscale\n"
"Australia: Document Number")
document_type = fields.Selection(string='Document Type', selection=[
('ID', 'ID'),
('PASSPORT', 'Passport'),
('VISA', 'Visa'),
('DRIVINGLICENSE', 'Driving license'),
], default='ID')
# Business
legal_business_name = fields.Char('Legal Business Name')
doing_business_as = fields.Char('Doing Business As')
registration_number = fields.Char('Registration Number')
# KYC
kyc_status = fields.Selection(string='KYC Status', selection=[
('awaiting_data', 'Data to provide'),
('pending', 'Waiting for validation'),
('passed', 'Confirmed'),
('failed', 'Failed'),
], required=True, default='pending')
kyc_status_message = fields.Char('KYC Status Message', readonly=True)
_sql_constraints = [
('adyen_uuid_uniq', 'UNIQUE(adyen_uuid)', 'Adyen UUID should be unique'),
]
@api.depends('transaction_ids')
def _compute_transactions_count(self):
for adyen_account_id in self:
adyen_account_id.transactions_count = len(adyen_account_id.transaction_ids)
@api.depends('first_name', 'last_name', 'legal_business_name')
def _compute_full_name(self):
for adyen_account_id in self:
if adyen_account_id.is_business:
adyen_account_id.full_name = adyen_account_id.legal_business_name
else:
adyen_account_id.full_name = "%s %s" % (adyen_account_id.first_name, adyen_account_id.last_name)
@api.model
def create(self, values):
adyen_account_id = super(AdyenAccount, self).create(values)
self.env.company.adyen_account_id = adyen_account_id.id
# Create account on odoo.com, proxy and Adyen
response = adyen_account_id._adyen_rpc('create_account_holder', adyen_account_id._format_data())
# Save adyen_uuid and proxy_token, that have been generated by odoo.com and the proxy
adyen_account_id.with_context(update_from_adyen=True).write({
'adyen_uuid': response['adyen_uuid'],
'proxy_token': response['proxy_token'],
})
# A default payout is created for all adyen accounts
adyen_account_id.env['adyen.payout'].with_context(update_from_adyen=True).create({
'code': response['adyen_response']['accountCode'],
'adyen_account_id': adyen_account_id.id,
})
return adyen_account_id
def write(self, vals):
res = super(AdyenAccount, self).write(vals)
if not self.env.context.get('update_from_adyen'):
self._adyen_rpc('update_account_holder', self._format_data())
return res
def unlink(self):
for adyen_account_id in self:
adyen_account_id._adyen_rpc('close_account_holder', {
'accountHolderCode': adyen_account_id.account_holder_code,
})
return super(AdyenAccount, self).unlink()
@api.model
def action_create_redirect(self):
'''
Accessing the FormView to create an Adyen account needs to be done through this action.
The action will redirect the user to accounts.odoo.com to link an Odoo user_id to the Adyen
account. After logging in on odoo.com the user will be redirected to his DB with a token in
the URL. This token is then needed to create the Adyen account.
'''
if self.env.company.adyen_account_id:
# An account already exists, show it
return {
'name': _('Adyen Account'),
'view_mode': 'form',
'res_model': 'adyen.account',
'res_id': self.env.company.adyen_account_id.id,
'type': 'ir.actions.act_window',
}
return_url = url_join(self.env['ir.config_parameter'].sudo().get_param('web.base.url'), 'adyen_platforms/create_account')
onboarding_url = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.onboarding_url')
return {
'type': 'ir.actions.act_url',
'url': url_join(onboarding_url, 'get_creation_token?return_url=%s' % return_url),
}
def action_show_transactions(self):
return {
'name': _('Transactions'),
'view_mode': 'tree,form',
'domain': [('adyen_account_id', '=', self.id)],
'res_model': 'adyen.transaction',
'type': 'ir.actions.act_window',
'context': {'group_by': ['adyen_payout_id']}
}
def _upload_photo_id(self, document_type, content, filename):
self._adyen_rpc('upload_document', {
'documentDetail': {
'accountHolderCode': self.account_holder_code,
'documentType': document_type,
'filename': filename,
},
'documentContent': content.decode(),
})
def _format_data(self):
data = {
'accountHolderCode': self.account_holder_code,
'accountHolderDetails': {
'address': {
'country': self.country_id.code,
'stateOrProvince': self.state_id.code or None,
'city': self.city,
'postalCode': self.zip,
'street': self.street,
'houseNumberOrName': self.house_number_or_name,
},
'email': self.email,
'fullPhoneNumber': self.phone_number,
},
'legalEntity': 'Business' if self.is_business else 'Individual',
}
if self.is_business:
data['accountHolderDetails']['businessDetails'] = {
'legalBusinessName': self.legal_business_name,
'doingBusinessAs': self.doing_business_as,
'registrationNumber': self.registration_number,
}
else:
data['accountHolderDetails']['individualDetails'] = {
'name': {
'firstName': self.first_name,
'lastName': self.last_name,
'gender': 'UNKNOWN',
},
'personalData': {
'dateOfBirth': str(self.date_of_birth),
}
}
# documentData cannot be present in the data if not set
if self.document_number:
data['accountHolderDetails']['individualDetails']['personalData']['documentData'] = [{
'number': self.document_number,
'type': self.document_type,
}]
return data
def _adyen_rpc(self, operation, adyen_data={}):
if operation == 'create_account_holder':
url = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.onboarding_url')
params = {
'creation_token': request.session.get('adyen_creation_token'),
'adyen_data': adyen_data,
}
auth = None
else:
url = self.env['ir.config_parameter'].sudo().get_param('adyen_platforms.proxy_url')
params = {
'adyen_uuid': self.adyen_uuid,
'adyen_data': adyen_data,
}
auth = AdyenProxyAuth(self)
payload = {
'jsonrpc': '2.0',
'params': params,
}
try:
req = requests.post(url_join(url, operation), json=payload, auth=auth, timeout=TIMEOUT)
req.raise_for_status()
except requests.exceptions.Timeout:
raise UserError(_('A timeout occured whil trying to reach the Adyen proxy.'))
except Exception as e:
raise UserError(_('The Adyen proxy is not reachable, please try again later.'))
response = req.json()
if 'error' in response:
name = response['error']['data'].get('name').rpartition('.')[-1]
if name == 'ValidationError':
raise ValidationError(response['error']['data'].get('arguments')[0])
else:
raise UserError(_("We had troubles reaching Adyen, please retry later or contact the support if the problem persists"))
result = response.get('result')
if 'verification' in result:
self._update_kyc_status(result['verification'])
return result
@api.model
def _sync_adyen_cron(self):
self._sync_adyen_kyc_status()
self.env['adyen.transaction'].sync_adyen_transactions()
self.env['adyen.payout']._process_payouts()
@api.model
def _sync_adyen_kyc_status(self):
for adyen_account_id in self.search([]):
data = adyen_account_id._adyen_rpc('get_account_holder', {
'accountHolderCode': adyen_account_id.account_holder_code,
})
adyen_account_id._update_kyc_status(data['verification'])
def _update_kyc_status(self, checks):
all_checks_status = []
# Account Holder Checks
account_holder_checks = checks.get('accountHolder', {})
account_holder_messages = []
for check in account_holder_checks.get('checks'):
all_checks_status.append(check['status'])
kyc_status_message = self._get_kyc_message(check)
if kyc_status_message:
account_holder_messages.append(kyc_status_message)
# Shareholders Checks
shareholder_checks = checks.get('shareholders', {})
shareholder_messages = []
kyc_status_message = False
for sc in shareholder_checks:
shareholder_status = []
shareholder_id = self.shareholder_ids.filtered(lambda shareholder: shareholder.shareholder_uuid == sc['shareholderCode'])
for check in sc.get('checks'):
all_checks_status.append(check['status'])
shareholder_status.append(check['status'])
kyc_status_message = self._get_kyc_message(check)
if kyc_status_message:
shareholder_messages.append('[%s] %s' % (shareholder_id.display_name, kyc_status_message))
shareholder_id.with_context(update_from_adyen=True).write({
'kyc_status': self.get_status(shareholder_status),
'kyc_status_message': kyc_status_message,
})
# Bank Account Checks
bank_account_checks = checks.get('bankAccounts', {})
bank_account_messages = []
kyc_status_message = False
for bac in bank_account_checks:
bank_account_status = []
bank_account_id = self.bank_account_ids.filtered(lambda bank_account: bank_account.bank_account_uuid == bac['bankAccountUUID'])
for check in bac.get('checks'):
all_checks_status.append(check['status'])
bank_account_status.append(check['status'])
kyc_status_message = self._get_kyc_message(check)
if kyc_status_message:
bank_account_messages.append('[%s] %s' % (bank_account_id.display_name, kyc_status_message))
bank_account_id.with_context(update_from_adyen=True).write({
'kyc_status': self.get_status(bank_account_status),
'kyc_status_message': kyc_status_message,
})
kyc_status = self.get_status(all_checks_status)
kyc_status_message = self.env['ir.qweb']._render('adyen_platforms.kyc_status_message', {
'kyc_status': dict(self._fields['kyc_status'].selection)[kyc_status],
'account_holder_messages': account_holder_messages,
'shareholder_messages': shareholder_messages,
'bank_account_messages': bank_account_messages,
})
if kyc_status_message.decode() != self.kyc_status_message:
self.sudo().message_post(body = kyc_status_message, subtype_xmlid="mail.mt_comment") # Message from Odoo Bot
self.with_context(update_from_adyen=True).write({
'kyc_status': kyc_status,
'kyc_status_message': kyc_status_message,
})
@api.model
def get_status(self, statuses):
if any(status in ['FAILED'] for status in statuses):
return 'failed'
if any(status in ['INVALID_DATA', 'RETRY_LIMIT_REACHED', 'AWAITING_DATA'] for status in statuses):
return 'awaiting_data'
if any(status in ['DATA_PROVIDED', 'PENDING'] for status in statuses):
return 'pending'
return 'passed'
@api.model
def _get_kyc_message(self, check):
if check.get('summary', {}).get('kycCheckDescription'):
return check['summary']['kycCheckDescription']
if check.get('requiredFields', {}):
return _('Missing required fields: ') + ', '.join(check.get('requiredFields'))
return ''
class AdyenShareholder(models.Model):
_name = 'adyen.shareholder'
_inherit = ['adyen.id.mixin', 'adyen.address.mixin']
_description = 'Adyen for Plaforms Shareholder'
_rec_name = 'full_name'
adyen_account_id = fields.Many2one('adyen.account', ondelete='cascade')
shareholder_reference = fields.Char('Reference', default=lambda self: uuid.uuid4().hex)
shareholder_uuid = fields.Char('UUID') # Given by Adyen
first_name = fields.Char('First Name', required=True)
last_name = fields.Char('Last Name', required=True)
full_name = fields.Char(compute='_compute_full_name')
date_of_birth = fields.Date('Date of birth', required=True)
document_number = fields.Char('ID Number',
help="The type of ID Number required depends on the country:\n"
"US: Social Security Number (9 digits or last 4 digits)\n"
"Canada: Social Insurance Number\nItaly: Codice fiscale\n"
"Australia: Document Number")
# KYC
kyc_status = fields.Selection(string='KYC Status', selection=[
('awaiting_data', 'Data to provide'),
('pending', 'Waiting for validation'),
('passed', 'Confirmed'),
('failed', 'Failed'),
], required=True, default='pending')
kyc_status_message = fields.Char('KYC Status Message', readonly=True)
@api.depends('first_name', 'last_name')
def _compute_full_name(self):
for adyen_shareholder_id in self:
adyen_shareholder_id.full_name = '%s %s' % (adyen_shareholder_id.first_name, adyen_shareholder_id.last_name)
@api.model
def create(self, values):
adyen_shareholder_id = super(AdyenShareholder, self).create(values)
response = adyen_shareholder_id.adyen_account_id._adyen_rpc('update_account_holder', adyen_shareholder_id._format_data())
shareholders = response['accountHolderDetails']['businessDetails']['shareholders']
created_shareholder = next(shareholder for shareholder in shareholders if shareholder['shareholderReference'] == adyen_shareholder_id.shareholder_reference)
adyen_shareholder_id.with_context(update_from_adyen=True).write({
'shareholder_uuid': created_shareholder['shareholderCode'],
})
return adyen_shareholder_id
def write(self, vals):
res = super(AdyenShareholder, self).write(vals)
if not self.env.context.get('update_from_adyen'):
self.adyen_account_id._adyen_rpc('update_account_holder', self._format_data())
return res
def unlink(self):
for shareholder_id in self:
shareholder_id.adyen_account_id._adyen_rpc('delete_shareholders', {
'accountHolderCode': shareholder_id.adyen_account_id.account_holder_code,
'shareholderCodes': [shareholder_id.shareholder_uuid],
})
return super(AdyenShareholder, self).unlink()
def _upload_photo_id(self, document_type, content, filename):
self.adyen_account_id._adyen_rpc('upload_document', {
'documentDetail': {
'accountHolderCode': self.adyen_account_id.account_holder_code,
'shareholderCode': self.shareholder_uuid,
'documentType': document_type,
'filename': filename,
},
'documentContent': content.decode(),
})
def _format_data(self):
data = {
'accountHolderCode': self.adyen_account_id.account_holder_code,
'accountHolderDetails': {
'businessDetails': {
'shareholders': [{
'shareholderCode': self.shareholder_uuid or None,
'shareholderReference': self.shareholder_reference,
'address': {
'city': self.city,
'country': self.country_code,
'houseNumberOrName': self.house_number_or_name,
'postalCode': self.zip,
'stateOrProvince': self.state_id.code or None,
'street': self.street,
},
'name': {
'firstName': self.first_name,
'lastName': self.last_name,
'gender': 'UNKNOWN'
},
'personalData': {
'dateOfBirth': str(self.date_of_birth),
}
}]
}
}
}
# documentData cannot be present in the data if not set
if self.document_number:
data['accountHolderDetails']['businessDetails']['shareholders'][0]['personalData']['documentData'] = [{
'number': self.document_number,
'type': 'ID',
}]
return data
class AdyenBankAccount(models.Model):
_name = 'adyen.bank.account'
_description = 'Adyen for Plaforms Bank Account'
adyen_account_id = fields.Many2one('adyen.account', ondelete='cascade')
bank_account_reference = fields.Char('Reference', default=lambda self: uuid.uuid4().hex)
bank_account_uuid = fields.Char('UUID') # Given by Adyen
owner_name = fields.Char('Owner Name', required=True)
country_id = fields.Many2one('res.country', string='Country', domain=[('code', 'in', ADYEN_AVAILABLE_COUNTRIES)], required=True)
country_code = fields.Char(related='country_id.code')
currency_id = fields.Many2one('res.currency', string='Currency', required=True)
iban = fields.Char('IBAN')
account_number = fields.Char('Account Number')
branch_code = fields.Char('Branch Code')
bank_code = fields.Char('Bank Code')
account_type = fields.Selection(string='Account Type', selection=[
('checking', 'Checking'),
('savings', 'Savings'),
])
owner_country_id = fields.Many2one('res.country', string='Owner Country')
owner_state_id = fields.Many2one('res.country.state', 'Owner State', domain="[('country_id', '=?', owner_country_id)]")
owner_street = fields.Char('Owner Street')
owner_city = fields.Char('Owner City')
owner_zip = fields.Char('Owner ZIP')
owner_house_number_or_name = fields.Char('Owner House Number or Name')
bank_statement = fields.Binary('Bank Statement', help="You need to provide a bank statement to allow payouts. \
The file must be a bank statement, a screenshot of your online banking environment, a letter from the bank or a cheque and must contain \
the logo of the bank or it's name in a unique font, the bank account details, the name of the account holder.\
Allowed formats: jpg, pdf, png. Maximum allowed size: 10MB.")
bank_statement_filename = fields.Char()
# KYC
kyc_status = fields.Selection(string='KYC Status', selection=[
('awaiting_data', 'Data to provide'),
('pending', 'Waiting for validation'),
('passed', 'Confirmed'),
('failed', 'Failed'),
], required=True, default='pending')
kyc_status_message = fields.Char('KYC Status Message', readonly=True)
@api.model
def create(self, values):
adyen_bank_account_id = super(AdyenBankAccount, self).create(values)
response = adyen_bank_account_id.adyen_account_id._adyen_rpc('update_account_holder', adyen_bank_account_id._format_data())
bank_accounts = response['accountHolderDetails']['bankAccountDetails']
created_bank_account = next(bank_account for bank_account in bank_accounts if bank_account['bankAccountReference'] == adyen_bank_account_id.bank_account_reference)
adyen_bank_account_id.with_context(update_from_adyen=True).write({
'bank_account_uuid': created_bank_account['bankAccountUUID'],
})
return adyen_bank_account_id
def write(self, vals):
res = super(AdyenBankAccount, self).write(vals)
if not self.env.context.get('update_from_adyen'):
self.adyen_account_id._adyen_rpc('update_account_holder', self._format_data())
if 'bank_statement' in vals:
self._upload_bank_statement(vals['bank_statement'], vals['bank_statement_filename'])
return res
def unlink(self):
for bank_account_id in self:
bank_account_id.adyen_account_id._adyen_rpc('delete_bank_accounts', {
'accountHolderCode': bank_account_id.adyen_account_id.account_holder_code,
'bankAccountUUIDs': [bank_account_id.bank_account_uuid],
})
return super(AdyenBankAccount, self).unlink()
def _format_data(self):
return {
'accountHolderCode': self.adyen_account_id.account_holder_code,
'accountHolderDetails': {
'bankAccountDetails': [{
'accountNumber': self.account_number or None,
'accountType': self.account_type or None,
'bankAccountReference': self.bank_account_reference,
'bankAccountUUID': self.bank_account_uuid or None,
'bankCode': self.bank_code or None,
'branchCode': self.branch_code or None,
'countryCode': self.country_code,
'currencyCode': self.currency_id.name,
'iban': self.iban or None,
'ownerCity': self.owner_city or None,
'ownerCountryCode': self.owner_country_id.code or None,
'ownerHouseNumberOrName': self.owner_house_number_or_name or None,
'ownerName': self.owner_name,
'ownerPostalCode': self.owner_zip or None,
'ownerState': self.owner_state_id.code or None,
'ownerStreet': self.owner_street or None,
}],
}
}
def _upload_bank_statement(self, content, filename):
file_extension = os.path.splitext(filename)[1]
file_size = len(content.encode('utf-8'))
if file_extension not in ['.jpeg', '.jpg', '.pdf', '.png']:
raise ValidationError(_('Allowed file formats for bank statements are jpeg, jpg, pdf or png'))
if file_size >> 20 > 10 or (file_size >> 10 < 10 and file_extension != '.pdf') :
raise ValidationError(_('Bank statements must be greater than 10kB (except for PDFs) and smaller than 10MB'))
self.adyen_account_id._adyen_rpc('upload_document', {
'documentDetail': {
'accountHolderCode': self.adyen_account_id.account_holder_code,
'bankAccountUUID': self.bank_account_uuid,
'documentType': 'BANK_STATEMENT',
'filename': filename,
},
'documentContent': content,
})
class AdyenPayout(models.Model):
_name = 'adyen.payout'
_description = 'Adyen for Plaforms Payout'
@api.depends('payout_schedule')
def _compute_next_scheduled_payout(self):
today = fields.date.today()
for adyen_payout_id in self:
adyen_payout_id.next_scheduled_payout = date_utils.end_of(today, adyen_payout_id.payout_schedule)
adyen_account_id = fields.Many2one('adyen.account', ondelete='cascade')
adyen_bank_account_id = fields.Many2one('adyen.bank.account', string='Bank Account',
help='The bank account to which the payout is to be made. If left blank, a bank account is automatically selected')
name = fields.Char('Name', default='Default', required=True)
code = fields.Char('Account Code')
payout_schedule = fields.Selection(string='Schedule', selection=[
('day', 'Daily'),
('week', 'Weekly'),
('month', 'Monthly'),
], default='week', required=True)
next_scheduled_payout = fields.Date('Next scheduled payout', compute=_compute_next_scheduled_payout, store=True)
transaction_ids = fields.One2many('adyen.transaction', 'adyen_payout_id', string='Transactions')
@api.model
def create(self, values):
adyen_payout_id = super(AdyenPayout, self).create(values)
if not adyen_payout_id.env.context.get('update_from_adyen'):
response = adyen_payout_id.adyen_account_id._adyen_rpc('create_payout', {
'accountHolderCode': adyen_payout_id.adyen_account_id.account_holder_code,
})
adyen_payout_id.with_context(update_from_adyen=True).write({
'code': response['accountCode'],
})
return adyen_payout_id
def unlink(self):
for adyen_payout_id in self:
adyen_payout_id.adyen_account_id._adyen_rpc('close_payout', {
'accountCode': adyen_payout_id.code,
})
return super(AdyenPayout, self).unlink()
@api.model
def _process_payouts(self):
for adyen_payout_id in self.search([('next_scheduled_payout', '<', fields.Date.today())]):
adyen_payout_id.send_payout_request(notify=False)
adyen_payout_id._compute_next_scheduled_payout()
def send_payout_request(self, notify=True):
response = self.adyen_account_id._adyen_rpc('account_holder_balance', {
'accountHolderCode': self.adyen_account_id.account_holder_code,
})
balances = next(account_balance['detailBalance']['balance'] for account_balance in response['balancePerAccount'] if account_balance['accountCode'] == self.code)
if notify and not balances:
self.env['bus.bus'].sendone(
(self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
{'type': 'simple_notification', 'title': _('No pending balance'), 'message': _('No balance is currently awaitng payout.')}
)
for balance in balances:
response = self.adyen_account_id._adyen_rpc('payout_request', {
'accountCode': self.code,
'accountHolderCode': self.adyen_account_id.account_holder_code,
'bankAccountUUID': self.adyen_bank_account_id.bank_account_uuid or None,
'amount': balance,
})
if notify and response['resultCode'] == 'Received':
currency_id = self.env['res.currency'].search([('name', '=', balance['currency'])])
value = round(balance['value'] / (10 ** currency_id.decimal_places), 2) # Convert from minor units
amount = str(value) + currency_id.symbol if currency_id.position == 'after' else currency_id.symbol + str(value)
message = _('Successfully sent payout request for %s', amount)
self.env['bus.bus'].sendone(
(self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
{'type': 'simple_notification', 'title': _('Payout Request sent'), 'message': message}
)
def _fetch_transactions(self, page=1):
response = self.adyen_account_id._adyen_rpc('get_transactions', {
'accountHolderCode': self.adyen_account_id.account_holder_code,
'transactionListsPerAccount': [{
'accountCode': self.code,
'page': page,
}]
})
transaction_list = response['accountTransactionLists'][0]
return transaction_list['transactions'], transaction_list['hasNextPage']