[MIG] 12.0 account_bank_statement_import_adyen, account_bank_statement_clearing_account

This commit is contained in:
Martin Pishpecki
2020-05-13 17:05:05 +02:00
committed by Ronald Portier (Therp BV)
parent ce6a6d6852
commit 9c7f36ad5d
18 changed files with 209 additions and 164 deletions

View File

@@ -1,69 +1,35 @@
.. 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
**This file is going to be generated by oca-gen-addon-readme.**
======================
Adyen statement import
======================
*Manual changes will be overwritten.*
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.
Please provide content in the ``readme`` directory:
Configuration
=============
* **DESCRIPTION.rst** (required)
* INSTALL.rst (optional)
* CONFIGURE.rst (optional)
* **USAGE.rst** (optional, highly recommended)
* DEVELOP.rst (optional)
* ROADMAP.rst (optional)
* HISTORY.rst (optional, recommended)
* **CONTRIBUTORS.rst** (optional, highly recommended)
* CREDITS.rst (optional)
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.
Content of this README will also be drawn from the addon manifest,
from keys such as name, authors, maintainers, development_status,
and license.
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
A good, one sentence summary in the manifest is also highly recommended.
Bug Tracker
===========
Automatic changelog generation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
`HISTORY.rst` can be auto generated using `towncrier <https://pypi.org/project/towncrier>`_.
Credits
=======
Just put towncrier compatible changelog fragments into `readme/newsfragments`
and the changelog file will be automatically generated and updated when a new fragment is added.
Images
------
Please refer to `towncrier` documentation to know more.
* 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.
NOTE: the changelog will be automatically generated when using `/ocabot merge $option`.
If you need to run it manually, refer to `OCA/maintainer-tools README <https://github.com/OCA/maintainer-tools>`_.

View File

@@ -0,0 +1,24 @@
# © 2017 Opener BV (<https://opener.amsterdam>)
# © 2020 Vanmoof BV (<https://www.vanmoof.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Adyen statement import",
"version": "12.0.1.0.0",
"author": "Opener BV, Vanmoof BV, Odoo Community Association (OCA)",
"category": "Banking addons",
"website": "https://github.com/oca/bank-statement-import",
"license": "AGPL-3",
"depends": [
"account_bank_statement_import",
"account_bank_statement_clearing_account",
],
"external_dependencies": {
"python": [
"openpyxl",
],
},
"data": [
"views/account_journal.xml",
],
"installable": True,
}

View File

@@ -1,18 +0,0 @@
# 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

@@ -1,19 +1,15 @@
# 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)
from odoo import api, models, fields
from odoo.exceptions import UserError
from odoo.tools.translate import _
class Import(models.TransientModel):
class AccountBankStatementImport(models.TransientModel):
_inherit = 'account.bank.statement.import'
@api.model
@@ -21,31 +17,25 @@ class Import(models.TransientModel):
"""Parse an Adyen xlsx file and map merchant account strings
to journals. """
try:
statements = self.import_adyen_xlsx(data_file)
return self.import_adyen_xlsx(data_file)
except ValueError:
return super(Import, self)._parse_file(data_file)
return super(AccountBankStatementImport, self)._parse_file(
data_file)
for statement in statements:
merchant_id = statement['account_number']
def _find_additional_data(self, currency_code, account_number):
""" Try to find journal by Adyen merchant account """
if account_number:
journal = self.env['account.journal'].search([
('adyen_merchant_account', '=', merchant_id)], limit=1)
('adyen_merchant_account', '=', account_number)], 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)
if self._context.get('journal_id', journal.id) != journal.id:
raise UserError(
_('Selected journal Merchant Account does not match '
'the import file Merchant Account '
'column: %s') % account_number)
self = self.with_context(journal_id=journal.id)
return super(AccountBankStatementImport, self)._find_additional_data(
currency_code, account_number)
@api.model
def balance(self, row):
@@ -54,14 +44,16 @@ class Import(models.TransientModel):
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
def import_adyen_transaction(self, statement, statement_id, row):
transaction_id = str(len(statement['transactions'])).zfill(4)
transaction = dict(
unique_import_id=statement_id + transaction_id,
date=fields.Date.from_string(row[6]),
amount=self.balance(row),
note='%s %s %s %s' % (row[2], row[3], row[4], row[21]),
name="%s" % (row[3] or row[4] or row[9]),
)
statement['transactions'].append(transaction)
@api.model
def import_adyen_xlsx(self, data_file):
@@ -71,6 +63,7 @@ class Import(models.TransientModel):
fees = 0.0
balance = 0.0
payout = 0.0
statement_id = None
with BytesIO() as buf:
buf.write(data_file)
@@ -94,22 +87,24 @@ class Import(models.TransientModel):
headers = True
continue
if not statement:
statement = BankStatement()
statement = {'transactions': []}
statements.append(statement)
statement.statement_id = '%s %s/%s' % (
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
currency_code = row[14]
merchant_id = row[2]
statement['name'] = '%s %s/%s' % (
row[2], row[6].year, row[23])
date = fields.Date.from_string(row[6])
if not statement.get('date') or statement.get('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)
self.import_adyen_transaction(statement, statement_id, row)
fees += sum(
row[i] if row[i] else 0.0
for i in (17, 18, 19, 20))
@@ -119,13 +114,15 @@ class Import(models.TransientModel):
'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
transaction_id = str(len(statement['transactions'])).zfill(4)
transaction = dict(
unique_import_id=statement_id + transaction_id,
date=max(t['date'] for t in statement['transactions']),
amount=-fees,
name='Commission, markup etc. batch %s' % (int(row[23])),
)
balance -= fees
transaction.message = 'Commision, markup etc. batch %s' % (
int(row[23]))
statement['transactions'].append(transaction)
if statement['transactions'] and not payout:
raise UserError(
@@ -135,5 +132,4 @@ class Import(models.TransientModel):
raise UserError(
_('Parse error. Balance %s not equal to merchant '
'payout %s') % (balance, payout))
return statements
return currency_code, merchant_id, statements

View File

@@ -1,5 +1,7 @@
# coding: utf-8
from openerp import fields, models
# © 2017 Opener BV (<https://opener.amsterdam>)
# © 2020 Vanmoof BV (<https://www.vanmoof.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class Journal(models.Model):
@@ -8,3 +10,9 @@ class Journal(models.Model):
adyen_merchant_account = fields.Char(
help=('Fill in the exact merchant account string to select this '
'journal when importing Adyen statements'))
def _get_bank_statements_available_import_formats(self):
res = super(
Journal, self)._get_bank_statements_available_import_formats()
res.append('adyen')
return res

View File

@@ -0,0 +1,3 @@
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.

View File

@@ -0,0 +1,2 @@
* Stefan Rijnhart <stefan@opener.amsterdam> (https://opener.amsterdam)
* Martin Pishpecki <pishpecki@gmail.com> (https://www.vanmoof.com)

View File

@@ -0,0 +1,10 @@
======================
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.

View File

@@ -0,0 +1,3 @@
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.

View File

@@ -1,34 +1,49 @@
# coding: utf-8
from openerp.addons.account_bank_statement_import.tests import (
TestStatementFile)
# © 2017 Opener BV (<https://opener.amsterdam>)
# © 2020 Vanmoof BV (<https://www.vanmoof.com>)
# © 2015 Therp BV (<http://therp.nl>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import base64
from odoo.exceptions import UserError
from odoo.tests.common import SavepointCase
from odoo.modules.module import get_module_resource
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({
class TestImportAdyen(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestImportAdyen, cls).setUpClass()
cls.journal = cls.env['account.journal'].create({
'company_id': cls.env.user.company_id.id,
'name': 'Adyen test',
'code': 'ADY',
'type': 'bank',
'adyen_merchant_account': 'YOURCOMPANY_ACCOUNT',
'update_posted': True,
'currency_id': cls.env.ref('base.USD').id,
})
# Enable reconcilation on the default journal account to trigger
# the functionality from account_bank_statement_clearing_account
cls.journal.default_debit_account_id.reconcile = True
def test_import_adyen(self):
def test_01_import_adyen(self):
""" Test that the Adyen statement can be imported and that the
lines on the default journal (clearing) account are fully reconciled
with each other """
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(statement.journal_id, self.journal)
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)
'internal_type', '=', 'receivable')], limit=1)
for line in statement.line_ids:
line.process_reconciliation([{
line.process_reconciliation(new_aml_dicts=[{
'debit': -line.amount if line.amount < 0 else 0,
'credit': line.amount if line.amount > 0 else 0,
'account_id': account.id}])
@@ -38,18 +53,56 @@ class TestImportAdyen(TestStatementFile):
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')
reconcile = lines.mapped('full_reconcile_id')
self.assertEqual(len(reconcile), 1)
self.assertFalse(lines.mapped('reconcile_partial_id'))
self.assertEqual(lines, reconcile.line_id)
self.assertEqual(lines, reconcile.reconciled_line_ids)
# Reset the bank statement to see the counterpart lines being
# unreconciled
statement.button_draft()
self.assertEqual(statement.state, 'draft')
self.assertFalse(lines.mapped('reconcile_partial_id'))
self.assertFalse(lines.mapped('reconcile_id'))
self.assertEqual(statement.state, 'open')
self.assertFalse(lines.mapped('matched_debit_ids'))
self.assertFalse(lines.mapped('matched_credit_ids'))
self.assertFalse(lines.mapped('full_reconcile_id'))
def test_import_adyen_credit_fees(self):
# Confirm the statement without the correct clearing account settings
self.journal.default_debit_account_id.reconcile = False
statement.button_confirm_bank()
self.assertEqual(statement.state, 'confirm')
self.assertFalse(lines.mapped('matched_debit_ids'))
self.assertFalse(lines.mapped('matched_credit_ids'))
self.assertFalse(lines.mapped('full_reconcile_id'))
def test_02_import_adyen_credit_fees(self):
""" Import an Adyen statement with credit fees """
self._test_statement_import(
'account_bank_statement_import_adyen',
'adyen_test_credit_fees.xlsx',
'YOURCOMPANY_ACCOUNT 2016/8')
def test_03_import_adyen_invalid(self):
""" Trying to hit that coverall target """
with self.assertRaisesRegex(UserError, 'Could not make sense'):
self._test_statement_import(
'account_bank_statement_import_adyen',
'adyen_test_invalid.xls',
'invalid')
def _test_statement_import(
self, module_name, file_name, statement_name):
"""Test correct creation of single statement."""
statement_path = get_module_resource(
module_name,
'test_files',
file_name
)
statement_file = open(statement_path, 'rb').read()
import_wizard = self.env['account.bank.statement.import'].create({
'data_file': base64.b64encode(statement_file),
'filename': file_name})
import_wizard.import_file()
# statement name is account number + '-' + date of last line:
statements = self.env['account.bank.statement'].search(
[('name', '=', statement_name)])
self.assertTrue(statements)
return statements

View File

@@ -1,15 +1,13 @@
<?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>
<odoo>
<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="currency_id" position="after">
<field name="adyen_merchant_account"/>
</field>
</record>
</data>
</openerp>
</field>
</record>
</odoo>