diff --git a/account_bank_statement_import_adyen/README.rst b/account_bank_statement_import_adyen/README.rst index 9b6baaba..38929e87 100644 --- a/account_bank_statement_import_adyen/README.rst +++ b/account_bank_statement_import_adyen/README.rst @@ -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 -`_. 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 `_. -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 `_. - -Contributors ------------- - -* Stefan Rijnhart - -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 `_. diff --git a/account_bank_statement_import_adyen/__manifest__.py b/account_bank_statement_import_adyen/__manifest__.py new file mode 100644 index 00000000..4be97c2e --- /dev/null +++ b/account_bank_statement_import_adyen/__manifest__.py @@ -0,0 +1,24 @@ +# © 2017 Opener BV () +# © 2020 Vanmoof BV () +# 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, +} diff --git a/account_bank_statement_import_adyen/__openerp__.py b/account_bank_statement_import_adyen/__openerp__.py deleted file mode 100644 index 6f47e712..00000000 --- a/account_bank_statement_import_adyen/__openerp__.py +++ /dev/null @@ -1,18 +0,0 @@ -# coding: utf-8 -# © 2017 Opener BV () -# 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, -} diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index aae90b71..a8d17f60 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -1,19 +1,15 @@ -# coding: utf-8 # © 2017 Opener BV () # 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 diff --git a/account_bank_statement_import_adyen/models/account_journal.py b/account_bank_statement_import_adyen/models/account_journal.py index d0c431fd..44a3e7f8 100644 --- a/account_bank_statement_import_adyen/models/account_journal.py +++ b/account_bank_statement_import_adyen/models/account_journal.py @@ -1,5 +1,7 @@ -# coding: utf-8 -from openerp import fields, models +# © 2017 Opener BV () +# © 2020 Vanmoof BV () +# 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 diff --git a/account_bank_statement_import_adyen/readme/CONFIGURE.rst b/account_bank_statement_import_adyen/readme/CONFIGURE.rst new file mode 100644 index 00000000..7df8a95e --- /dev/null +++ b/account_bank_statement_import_adyen/readme/CONFIGURE.rst @@ -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. diff --git a/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst b/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..58e9f494 --- /dev/null +++ b/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Stefan Rijnhart (https://opener.amsterdam) +* Martin Pishpecki (https://www.vanmoof.com) diff --git a/account_bank_statement_import_adyen/readme/CREDITS.rst b/account_bank_statement_import_adyen/readme/CREDITS.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_import_adyen/readme/DESCRIPTION.rst b/account_bank_statement_import_adyen/readme/DESCRIPTION.rst new file mode 100644 index 00000000..c0832e16 --- /dev/null +++ b/account_bank_statement_import_adyen/readme/DESCRIPTION.rst @@ -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. diff --git a/account_bank_statement_import_adyen/readme/HISTORY.rst b/account_bank_statement_import_adyen/readme/HISTORY.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_import_adyen/readme/INSTALL.rst b/account_bank_statement_import_adyen/readme/INSTALL.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_import_adyen/readme/ROADMAP.rst b/account_bank_statement_import_adyen/readme/ROADMAP.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_import_adyen/readme/USAGE.rst b/account_bank_statement_import_adyen/readme/USAGE.rst new file mode 100644 index 00000000..70545a9f --- /dev/null +++ b/account_bank_statement_import_adyen/readme/USAGE.rst @@ -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. diff --git a/account_bank_statement_import_adyen/test_files/adyen_test.xlsx b/account_bank_statement_import_adyen/test_files/adyen_test.xlsx index 3adaa8db..b7cd4c6c 100644 Binary files a/account_bank_statement_import_adyen/test_files/adyen_test.xlsx and b/account_bank_statement_import_adyen/test_files/adyen_test.xlsx differ diff --git a/account_bank_statement_import_adyen/test_files/adyen_test_credit_fees.xlsx b/account_bank_statement_import_adyen/test_files/adyen_test_credit_fees.xlsx index 0443fceb..c4a786de 100644 Binary files a/account_bank_statement_import_adyen/test_files/adyen_test_credit_fees.xlsx and b/account_bank_statement_import_adyen/test_files/adyen_test_credit_fees.xlsx differ diff --git a/account_bank_statement_import_adyen/test_files/adyen_test_invalid.xls b/account_bank_statement_import_adyen/test_files/adyen_test_invalid.xls new file mode 100644 index 00000000..c7e3eb28 Binary files /dev/null and b/account_bank_statement_import_adyen/test_files/adyen_test_invalid.xls differ diff --git a/account_bank_statement_import_adyen/tests/test_import_adyen.py b/account_bank_statement_import_adyen/tests/test_import_adyen.py index d4e8a684..acbadfdf 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -1,34 +1,49 @@ -# coding: utf-8 -from openerp.addons.account_bank_statement_import.tests import ( - TestStatementFile) +# © 2017 Opener BV () +# © 2020 Vanmoof BV () +# © 2015 Therp BV () +# 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 diff --git a/account_bank_statement_import_adyen/views/account_journal.xml b/account_bank_statement_import_adyen/views/account_journal.xml index 2e6f2e6b..02f36f05 100644 --- a/account_bank_statement_import_adyen/views/account_journal.xml +++ b/account_bank_statement_import_adyen/views/account_journal.xml @@ -1,15 +1,13 @@ - - - - Add Adyen merchant account - account.journal - - - - - + + + Add Adyen merchant account + account.journal + + + + - - - + + +