[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 **This file is going to be generated by oca-gen-addon-readme.**
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
====================== *Manual changes will be overwritten.*
Adyen statement import
======================
This module processes Adyen transaction statements in xlsx format. You can Please provide content in the ``readme`` directory:
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 * **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 Content of this README will also be drawn from the addon manifest,
Adyen clearing account as the default ledger account. Set your merchant from keys such as name, authors, maintainers, development_status,
account string in the Advanced settings on the journal form. and license.
Usage A good, one sentence summary in the manifest is also highly recommended.
=====
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 Automatic changelog generation
=========== ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Bugs are tracked on `GitHub Issues `HISTORY.rst` can be auto generated using `towncrier <https://pypi.org/project/towncrier>`_.
<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 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>`_. 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>`_.
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,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>) # © 2017 Opener BV (<https://opener.amsterdam>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from io import BytesIO from io import BytesIO
from openpyxl import load_workbook from openpyxl import load_workbook
from zipfile import BadZipfile from zipfile import BadZipfile
from openerp import models, api from odoo import api, models, fields
from openerp.exceptions import Warning as UserError from odoo.exceptions import UserError
from openerp.tools.misc import DEFAULT_SERVER_DATE_FORMAT as DATEFMT from odoo.tools.translate import _
from openerp.tools.translate import _
from openerp.addons.account_bank_statement_import.parserlib import (
BankStatement)
class Import(models.TransientModel): class AccountBankStatementImport(models.TransientModel):
_inherit = 'account.bank.statement.import' _inherit = 'account.bank.statement.import'
@api.model @api.model
@@ -21,31 +17,25 @@ class Import(models.TransientModel):
"""Parse an Adyen xlsx file and map merchant account strings """Parse an Adyen xlsx file and map merchant account strings
to journals. """ to journals. """
try: try:
statements = self.import_adyen_xlsx(data_file) return self.import_adyen_xlsx(data_file)
except ValueError: except ValueError:
return super(Import, self)._parse_file(data_file) return super(AccountBankStatementImport, self)._parse_file(
data_file)
for statement in statements: def _find_additional_data(self, currency_code, account_number):
merchant_id = statement['account_number'] """ Try to find journal by Adyen merchant account """
if account_number:
journal = self.env['account.journal'].search([ journal = self.env['account.journal'].search([
('adyen_merchant_account', '=', merchant_id)], limit=1) ('adyen_merchant_account', '=', account_number)], limit=1)
if journal: if journal:
statement['adyen_journal_id'] = journal.id if self._context.get('journal_id', journal.id) != journal.id:
else: raise UserError(
raise UserError( _('Selected journal Merchant Account does not match '
_('Please create a journal with merchant account "%s"') % 'the import file Merchant Account '
merchant_id) 'column: %s') % account_number)
statement['account_number'] = False self = self.with_context(journal_id=journal.id)
return statements return super(AccountBankStatementImport, self)._find_additional_data(
currency_code, account_number)
@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 @api.model
def balance(self, row): def balance(self, row):
@@ -54,14 +44,16 @@ class Import(models.TransientModel):
for i in (16, 17, 18, 19, 20)) for i in (16, 17, 18, 19, 20))
@api.model @api.model
def import_adyen_transaction(self, statement, row): def import_adyen_transaction(self, statement, statement_id, row):
transaction = statement.create_transaction() transaction_id = str(len(statement['transactions'])).zfill(4)
transaction.value_date = row[6].strftime(DATEFMT) transaction = dict(
transaction.transferred_amount = self.balance(row) unique_import_id=statement_id + transaction_id,
transaction.note = ( date=fields.Date.from_string(row[6]),
'%s %s %s %s' % (row[2], row[3], row[4], row[21])) amount=self.balance(row),
transaction.message = "%s" % (row[3] or row[4] or row[9]) note='%s %s %s %s' % (row[2], row[3], row[4], row[21]),
return transaction name="%s" % (row[3] or row[4] or row[9]),
)
statement['transactions'].append(transaction)
@api.model @api.model
def import_adyen_xlsx(self, data_file): def import_adyen_xlsx(self, data_file):
@@ -71,6 +63,7 @@ class Import(models.TransientModel):
fees = 0.0 fees = 0.0
balance = 0.0 balance = 0.0
payout = 0.0 payout = 0.0
statement_id = None
with BytesIO() as buf: with BytesIO() as buf:
buf.write(data_file) buf.write(data_file)
@@ -94,22 +87,24 @@ class Import(models.TransientModel):
headers = True headers = True
continue continue
if not statement: if not statement:
statement = BankStatement() statement = {'transactions': []}
statements.append(statement) statements.append(statement)
statement.statement_id = '%s %s/%s' % ( statement_id = '%s %s/%s' % (
row[2], row[6].strftime('%Y'), int(row[23])) row[2], row[6].strftime('%Y'), int(row[23]))
statement.local_currency = row[14] currency_code = row[14]
statement.local_account = row[2] merchant_id = row[2]
date = row[6].strftime(DATEFMT) statement['name'] = '%s %s/%s' % (
if not statement.date or statement.date > date: row[2], row[6].year, row[23])
statement.date = date 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() row[8] = row[8].strip()
if row[8] == 'MerchantPayout': if row[8] == 'MerchantPayout':
payout -= self.balance(row) payout -= self.balance(row)
else: else:
balance += self.balance(row) balance += self.balance(row)
self.import_adyen_transaction(statement, row) self.import_adyen_transaction(statement, statement_id, row)
fees += sum( fees += sum(
row[i] if row[i] else 0.0 row[i] if row[i] else 0.0
for i in (17, 18, 19, 20)) for i in (17, 18, 19, 20))
@@ -119,13 +114,15 @@ class Import(models.TransientModel):
'Not an Adyen statement. Did not encounter header row.') 'Not an Adyen statement. Did not encounter header row.')
if fees: if fees:
transaction = statement.create_transaction() transaction_id = str(len(statement['transactions'])).zfill(4)
transaction.value_date = max( transaction = dict(
t.value_date for t in statement['transactions']) unique_import_id=statement_id + transaction_id,
transaction.transferred_amount = -fees date=max(t['date'] for t in statement['transactions']),
amount=-fees,
name='Commission, markup etc. batch %s' % (int(row[23])),
)
balance -= fees balance -= fees
transaction.message = 'Commision, markup etc. batch %s' % ( statement['transactions'].append(transaction)
int(row[23]))
if statement['transactions'] and not payout: if statement['transactions'] and not payout:
raise UserError( raise UserError(
@@ -135,5 +132,4 @@ class Import(models.TransientModel):
raise UserError( raise UserError(
_('Parse error. Balance %s not equal to merchant ' _('Parse error. Balance %s not equal to merchant '
'payout %s') % (balance, payout)) 'payout %s') % (balance, payout))
return currency_code, merchant_id, statements
return statements

View File

@@ -1,5 +1,7 @@
# coding: utf-8 # © 2017 Opener BV (<https://opener.amsterdam>)
from openerp import fields, models # © 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): class Journal(models.Model):
@@ -8,3 +10,9 @@ class Journal(models.Model):
adyen_merchant_account = fields.Char( adyen_merchant_account = fields.Char(
help=('Fill in the exact merchant account string to select this ' help=('Fill in the exact merchant account string to select this '
'journal when importing Adyen statements')) '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 # © 2017 Opener BV (<https://opener.amsterdam>)
from openerp.addons.account_bank_statement_import.tests import ( # © 2020 Vanmoof BV (<https://www.vanmoof.com>)
TestStatementFile) # © 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): class TestImportAdyen(SavepointCase):
def setUp(self): @classmethod
super(TestImportAdyen, self).setUp() def setUpClass(cls):
self.journal = self.env['account.journal'].search( super(TestImportAdyen, cls).setUpClass()
[('type', '=', 'bank')], limit=1) cls.journal = cls.env['account.journal'].create({
self.journal.default_debit_account_id.reconcile = True 'company_id': cls.env.user.company_id.id,
self.journal.write({ 'name': 'Adyen test',
'code': 'ADY',
'type': 'bank',
'adyen_merchant_account': 'YOURCOMPANY_ACCOUNT', 'adyen_merchant_account': 'YOURCOMPANY_ACCOUNT',
'update_posted': True, '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( self._test_statement_import(
'account_bank_statement_import_adyen', 'adyen_test.xlsx', 'account_bank_statement_import_adyen', 'adyen_test.xlsx',
'YOURCOMPANY_ACCOUNT 2016/48') 'YOURCOMPANY_ACCOUNT 2016/48')
statement = self.env['account.bank.statement'].search( statement = self.env['account.bank.statement'].search(
[], order='create_date desc', limit=1) [], order='create_date desc', limit=1)
self.assertEqual(statement.journal_id, self.journal)
self.assertEqual(len(statement.line_ids), 22) self.assertEqual(len(statement.line_ids), 22)
self.assertTrue( self.assertTrue(
self.env.user.company_id.currency_id.is_zero( self.env.user.company_id.currency_id.is_zero(
sum(line.amount for line in statement.line_ids))) sum(line.amount for line in statement.line_ids)))
account = self.env['account.account'].search([( account = self.env['account.account'].search([(
'type', '=', 'receivable')], limit=1) 'internal_type', '=', 'receivable')], limit=1)
for line in statement.line_ids: for line in statement.line_ids:
line.process_reconciliation([{ line.process_reconciliation(new_aml_dicts=[{
'debit': -line.amount if line.amount < 0 else 0, 'debit': -line.amount if line.amount < 0 else 0,
'credit': line.amount if line.amount > 0 else 0, 'credit': line.amount if line.amount > 0 else 0,
'account_id': account.id}]) 'account_id': account.id}])
@@ -38,18 +53,56 @@ class TestImportAdyen(TestStatementFile):
lines = self.env['account.move.line'].search([ lines = self.env['account.move.line'].search([
('account_id', '=', self.journal.default_debit_account_id.id), ('account_id', '=', self.journal.default_debit_account_id.id),
('statement_id', '=', statement.id)]) ('statement_id', '=', statement.id)])
reconcile = lines.mapped('reconcile_id') reconcile = lines.mapped('full_reconcile_id')
self.assertEqual(len(reconcile), 1) self.assertEqual(len(reconcile), 1)
self.assertFalse(lines.mapped('reconcile_partial_id')) self.assertEqual(lines, reconcile.reconciled_line_ids)
self.assertEqual(lines, reconcile.line_id)
# Reset the bank statement to see the counterpart lines being
# unreconciled
statement.button_draft() statement.button_draft()
self.assertEqual(statement.state, 'draft') self.assertEqual(statement.state, 'open')
self.assertFalse(lines.mapped('reconcile_partial_id')) self.assertFalse(lines.mapped('matched_debit_ids'))
self.assertFalse(lines.mapped('reconcile_id')) 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( self._test_statement_import(
'account_bank_statement_import_adyen', 'account_bank_statement_import_adyen',
'adyen_test_credit_fees.xlsx', 'adyen_test_credit_fees.xlsx',
'YOURCOMPANY_ACCOUNT 2016/8') '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"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <odoo>
<data> <record id="view_account_journal_form" model="ir.ui.view">
<record id="view_account_journal_form" model="ir.ui.view"> <field name="name">Add Adyen merchant account</field>
<field name="name">Add Adyen merchant account</field> <field name="model">account.journal</field>
<field name="model">account.journal</field> <field name="inherit_id" ref="account.view_account_journal_form"/>
<field name="inherit_id" ref="account.view_account_journal_form"/> <field name="arch" type="xml">
<field name="arch" type="xml"> <field name="currency_id" position="after">
<field name="sequence_id" position="after"> <field name="adyen_merchant_account"/>
<field name="adyen_merchant_account"/>
</field>
</field> </field>
</record> </field>
</data> </record>
</openerp> </odoo>