mirror of
https://github.com/OCA/bank-statement-import.git
synced 2025-01-20 12:37:43 +02:00
[ADD] Adyen statement import
This commit is contained in:
committed by
Ronald Portier (Therp BV)
parent
f625bd6293
commit
ce6a6d6852
69
account_bank_statement_import_adyen/README.rst
Normal file
69
account_bank_statement_import_adyen/README.rst
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||||
|
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||||
|
:alt: License: AGPL-3
|
||||||
|
|
||||||
|
======================
|
||||||
|
Adyen statement import
|
||||||
|
======================
|
||||||
|
|
||||||
|
This module processes Adyen transaction statements in xlsx format. You can
|
||||||
|
import the statements in a dedicated journal. Reconcile your sale invoices
|
||||||
|
with the credit transations. Reconcile the aggregated counterpart
|
||||||
|
transaction with the transaction in your real bank journal and register the
|
||||||
|
aggregated fee line containing commision and markup on the applicable
|
||||||
|
cost account.
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
Configure a pseudo bank journal by creating a new journal with a dedicated
|
||||||
|
Adyen clearing account as the default ledger account. Set your merchant
|
||||||
|
account string in the Advanced settings on the journal form.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
After installing this module, you can import your Adyen transaction statements
|
||||||
|
through Menu Finance -> Bank -> Import. Don't enter a journal in the import
|
||||||
|
wizard.
|
||||||
|
|
||||||
|
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||||
|
:alt: Try me on Runbot
|
||||||
|
:target: https://runbot.odoo-community.org/runbot/174/8.0
|
||||||
|
|
||||||
|
|
||||||
|
Bug Tracker
|
||||||
|
===========
|
||||||
|
|
||||||
|
Bugs are tracked on `GitHub Issues
|
||||||
|
<https://github.com/OCA/bank-statement-import/issues>`_. In case of trouble, please
|
||||||
|
check there if your issue has already been reported. If you spotted it first,
|
||||||
|
help us smash it by providing detailed and welcomed feedback.
|
||||||
|
|
||||||
|
Credits
|
||||||
|
=======
|
||||||
|
|
||||||
|
Images
|
||||||
|
------
|
||||||
|
|
||||||
|
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
------------
|
||||||
|
|
||||||
|
* Stefan Rijnhart <stefan@opener.am>
|
||||||
|
|
||||||
|
Maintainer
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. image:: https://odoo-community.org/logo.png
|
||||||
|
:alt: Odoo Community Association
|
||||||
|
:target: https://odoo-community.org
|
||||||
|
|
||||||
|
This module is maintained by the OCA.
|
||||||
|
|
||||||
|
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||||
|
mission is to support the collaborative development of Odoo features and
|
||||||
|
promote its widespread use.
|
||||||
|
|
||||||
|
To contribute to this module, please visit https://odoo-community.org.
|
||||||
1
account_bank_statement_import_adyen/__init__.py
Normal file
1
account_bank_statement_import_adyen/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
18
account_bank_statement_import_adyen/__openerp__.py
Normal file
18
account_bank_statement_import_adyen/__openerp__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
# © 2017 Opener BV (<https://opener.amsterdam>)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
{
|
||||||
|
'name': 'Adyen statement import',
|
||||||
|
'version': '8.0.1.0.0',
|
||||||
|
'author': 'Opener BV, Odoo Community Association (OCA)',
|
||||||
|
'category': 'Banking addons',
|
||||||
|
'website': 'https://github.com/oca/bank-statement-import',
|
||||||
|
'depends': [
|
||||||
|
'account_bank_statement_import',
|
||||||
|
'account_bank_statement_clearing_account',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'views/account_journal.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
}
|
||||||
2
account_bank_statement_import_adyen/models/__init__.py
Normal file
2
account_bank_statement_import_adyen/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import account_bank_statement_import
|
||||||
|
from . import account_journal
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
# © 2017 Opener BV (<https://opener.amsterdam>)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
from io import BytesIO
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
from zipfile import BadZipfile
|
||||||
|
|
||||||
|
from openerp import models, api
|
||||||
|
from openerp.exceptions import Warning as UserError
|
||||||
|
from openerp.tools.misc import DEFAULT_SERVER_DATE_FORMAT as DATEFMT
|
||||||
|
from openerp.tools.translate import _
|
||||||
|
from openerp.addons.account_bank_statement_import.parserlib import (
|
||||||
|
BankStatement)
|
||||||
|
|
||||||
|
|
||||||
|
class Import(models.TransientModel):
|
||||||
|
_inherit = 'account.bank.statement.import'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _parse_file(self, data_file):
|
||||||
|
"""Parse an Adyen xlsx file and map merchant account strings
|
||||||
|
to journals. """
|
||||||
|
try:
|
||||||
|
statements = self.import_adyen_xlsx(data_file)
|
||||||
|
except ValueError:
|
||||||
|
return super(Import, self)._parse_file(data_file)
|
||||||
|
|
||||||
|
for statement in statements:
|
||||||
|
merchant_id = statement['account_number']
|
||||||
|
journal = self.env['account.journal'].search([
|
||||||
|
('adyen_merchant_account', '=', merchant_id)], limit=1)
|
||||||
|
if journal:
|
||||||
|
statement['adyen_journal_id'] = journal.id
|
||||||
|
else:
|
||||||
|
raise UserError(
|
||||||
|
_('Please create a journal with merchant account "%s"') %
|
||||||
|
merchant_id)
|
||||||
|
statement['account_number'] = False
|
||||||
|
return statements
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _import_statement(self, stmt_vals):
|
||||||
|
""" Propagate found journal to context, fromwhere it is picked up
|
||||||
|
in _get_journal """
|
||||||
|
journal_id = stmt_vals.pop('adyen_journal_id', None)
|
||||||
|
if journal_id:
|
||||||
|
self = self.with_context(journal_id=journal_id)
|
||||||
|
return super(Import, self)._import_statement(stmt_vals)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def balance(self, row):
|
||||||
|
return -(row[15] or 0) + sum(
|
||||||
|
row[i] if row[i] else 0.0
|
||||||
|
for i in (16, 17, 18, 19, 20))
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def import_adyen_transaction(self, statement, row):
|
||||||
|
transaction = statement.create_transaction()
|
||||||
|
transaction.value_date = row[6].strftime(DATEFMT)
|
||||||
|
transaction.transferred_amount = self.balance(row)
|
||||||
|
transaction.note = (
|
||||||
|
'%s %s %s %s' % (row[2], row[3], row[4], row[21]))
|
||||||
|
transaction.message = "%s" % (row[3] or row[4] or row[9])
|
||||||
|
return transaction
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def import_adyen_xlsx(self, data_file):
|
||||||
|
statements = []
|
||||||
|
statement = None
|
||||||
|
headers = False
|
||||||
|
fees = 0.0
|
||||||
|
balance = 0.0
|
||||||
|
payout = 0.0
|
||||||
|
|
||||||
|
with BytesIO() as buf:
|
||||||
|
buf.write(data_file)
|
||||||
|
try:
|
||||||
|
sheet = load_workbook(buf)._sheets[0]
|
||||||
|
except BadZipfile as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
for row in sheet.rows:
|
||||||
|
row = [cell.value for cell in row]
|
||||||
|
if len(row) != 31:
|
||||||
|
raise ValueError(
|
||||||
|
'Not an Adyen statement. Unexpected row length %s '
|
||||||
|
'instead of 31' % len(row))
|
||||||
|
if not row[1]:
|
||||||
|
continue
|
||||||
|
if not headers:
|
||||||
|
if row[1] != 'Company Account':
|
||||||
|
raise ValueError(
|
||||||
|
'Not an Adyen statement. Unexpected header "%s" '
|
||||||
|
'instead of "Company Account"', row[1])
|
||||||
|
headers = True
|
||||||
|
continue
|
||||||
|
if not statement:
|
||||||
|
statement = BankStatement()
|
||||||
|
statements.append(statement)
|
||||||
|
statement.statement_id = '%s %s/%s' % (
|
||||||
|
row[2], row[6].strftime('%Y'), int(row[23]))
|
||||||
|
statement.local_currency = row[14]
|
||||||
|
statement.local_account = row[2]
|
||||||
|
date = row[6].strftime(DATEFMT)
|
||||||
|
if not statement.date or statement.date > date:
|
||||||
|
statement.date = date
|
||||||
|
|
||||||
|
row[8] = row[8].strip()
|
||||||
|
if row[8] == 'MerchantPayout':
|
||||||
|
payout -= self.balance(row)
|
||||||
|
else:
|
||||||
|
balance += self.balance(row)
|
||||||
|
self.import_adyen_transaction(statement, row)
|
||||||
|
fees += sum(
|
||||||
|
row[i] if row[i] else 0.0
|
||||||
|
for i in (17, 18, 19, 20))
|
||||||
|
|
||||||
|
if not headers:
|
||||||
|
raise ValueError(
|
||||||
|
'Not an Adyen statement. Did not encounter header row.')
|
||||||
|
|
||||||
|
if fees:
|
||||||
|
transaction = statement.create_transaction()
|
||||||
|
transaction.value_date = max(
|
||||||
|
t.value_date for t in statement['transactions'])
|
||||||
|
transaction.transferred_amount = -fees
|
||||||
|
balance -= fees
|
||||||
|
transaction.message = 'Commision, markup etc. batch %s' % (
|
||||||
|
int(row[23]))
|
||||||
|
|
||||||
|
if statement['transactions'] and not payout:
|
||||||
|
raise UserError(
|
||||||
|
_('No payout detected in Adyen statement.'))
|
||||||
|
if self.env.user.company_id.currency_id.compare_amounts(
|
||||||
|
balance, payout) != 0:
|
||||||
|
raise UserError(
|
||||||
|
_('Parse error. Balance %s not equal to merchant '
|
||||||
|
'payout %s') % (balance, payout))
|
||||||
|
|
||||||
|
return statements
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from openerp import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class Journal(models.Model):
|
||||||
|
_inherit = 'account.journal'
|
||||||
|
|
||||||
|
adyen_merchant_account = fields.Char(
|
||||||
|
help=('Fill in the exact merchant account string to select this '
|
||||||
|
'journal when importing Adyen statements'))
|
||||||
BIN
account_bank_statement_import_adyen/test_files/adyen_test.xlsx
Normal file
BIN
account_bank_statement_import_adyen/test_files/adyen_test.xlsx
Normal file
Binary file not shown.
Binary file not shown.
1
account_bank_statement_import_adyen/tests/__init__.py
Normal file
1
account_bank_statement_import_adyen/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_import_adyen
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from openerp.addons.account_bank_statement_import.tests import (
|
||||||
|
TestStatementFile)
|
||||||
|
|
||||||
|
|
||||||
|
class TestImportAdyen(TestStatementFile):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestImportAdyen, self).setUp()
|
||||||
|
self.journal = self.env['account.journal'].search(
|
||||||
|
[('type', '=', 'bank')], limit=1)
|
||||||
|
self.journal.default_debit_account_id.reconcile = True
|
||||||
|
self.journal.write({
|
||||||
|
'adyen_merchant_account': 'YOURCOMPANY_ACCOUNT',
|
||||||
|
'update_posted': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_import_adyen(self):
|
||||||
|
self._test_statement_import(
|
||||||
|
'account_bank_statement_import_adyen', 'adyen_test.xlsx',
|
||||||
|
'YOURCOMPANY_ACCOUNT 2016/48')
|
||||||
|
statement = self.env['account.bank.statement'].search(
|
||||||
|
[], order='create_date desc', limit=1)
|
||||||
|
self.assertEqual(len(statement.line_ids), 22)
|
||||||
|
self.assertTrue(
|
||||||
|
self.env.user.company_id.currency_id.is_zero(
|
||||||
|
sum(line.amount for line in statement.line_ids)))
|
||||||
|
|
||||||
|
account = self.env['account.account'].search([(
|
||||||
|
'type', '=', 'receivable')], limit=1)
|
||||||
|
for line in statement.line_ids:
|
||||||
|
line.process_reconciliation([{
|
||||||
|
'debit': -line.amount if line.amount < 0 else 0,
|
||||||
|
'credit': line.amount if line.amount > 0 else 0,
|
||||||
|
'account_id': account.id}])
|
||||||
|
|
||||||
|
statement.button_confirm_bank()
|
||||||
|
self.assertEqual(statement.state, 'confirm')
|
||||||
|
lines = self.env['account.move.line'].search([
|
||||||
|
('account_id', '=', self.journal.default_debit_account_id.id),
|
||||||
|
('statement_id', '=', statement.id)])
|
||||||
|
reconcile = lines.mapped('reconcile_id')
|
||||||
|
self.assertEqual(len(reconcile), 1)
|
||||||
|
self.assertFalse(lines.mapped('reconcile_partial_id'))
|
||||||
|
self.assertEqual(lines, reconcile.line_id)
|
||||||
|
|
||||||
|
statement.button_draft()
|
||||||
|
self.assertEqual(statement.state, 'draft')
|
||||||
|
self.assertFalse(lines.mapped('reconcile_partial_id'))
|
||||||
|
self.assertFalse(lines.mapped('reconcile_id'))
|
||||||
|
|
||||||
|
def test_import_adyen_credit_fees(self):
|
||||||
|
self._test_statement_import(
|
||||||
|
'account_bank_statement_import_adyen',
|
||||||
|
'adyen_test_credit_fees.xlsx',
|
||||||
|
'YOURCOMPANY_ACCOUNT 2016/8')
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<openerp>
|
||||||
|
<data>
|
||||||
|
<record id="view_account_journal_form" model="ir.ui.view">
|
||||||
|
<field name="name">Add Adyen merchant account</field>
|
||||||
|
<field name="model">account.journal</field>
|
||||||
|
<field name="inherit_id" ref="account.view_account_journal_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<field name="sequence_id" position="after">
|
||||||
|
<field name="adyen_merchant_account"/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</openerp>
|
||||||
Reference in New Issue
Block a user