[ADD] Adyen statement import

This commit is contained in:
Stefan Rijnhart
2017-02-28 18:45:32 +01:00
committed by Ronald Portier (Therp BV)
parent f625bd6293
commit ce6a6d6852
11 changed files with 310 additions and 0 deletions

View 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.

View File

@@ -0,0 +1 @@
from . import models

View 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,
}

View File

@@ -0,0 +1,2 @@
from . import account_bank_statement_import
from . import account_journal

View File

@@ -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

View File

@@ -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'))

View File

@@ -0,0 +1 @@
from . import test_import_adyen

View File

@@ -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')

View File

@@ -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>