mirror of
https://github.com/OCA/bank-statement-import.git
synced 2025-01-20 12:37:43 +02:00
[MIG] 12.0 account_bank_statement_import_adyen, account_bank_statement_clearing_account
This commit is contained in:
committed by
Ronald Portier (Therp BV)
parent
ce6a6d6852
commit
9c7f36ad5d
@@ -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>`_.
|
||||
|
||||
24
account_bank_statement_import_adyen/__manifest__.py
Normal file
24
account_bank_statement_import_adyen/__manifest__.py
Normal 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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
3
account_bank_statement_import_adyen/readme/CONFIGURE.rst
Normal file
3
account_bank_statement_import_adyen/readme/CONFIGURE.rst
Normal 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.
|
||||
@@ -0,0 +1,2 @@
|
||||
* Stefan Rijnhart <stefan@opener.amsterdam> (https://opener.amsterdam)
|
||||
* Martin Pishpecki <pishpecki@gmail.com> (https://www.vanmoof.com)
|
||||
10
account_bank_statement_import_adyen/readme/DESCRIPTION.rst
Normal file
10
account_bank_statement_import_adyen/readme/DESCRIPTION.rst
Normal 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.
|
||||
3
account_bank_statement_import_adyen/readme/USAGE.rst
Normal file
3
account_bank_statement_import_adyen/readme/USAGE.rst
Normal 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.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user