From 11cc8f467a312942a45d797db8165b2708a1fbf0 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Tue, 28 Feb 2017 18:45:32 +0100 Subject: [PATCH 01/24] [ADD] Adyen statement import --- .../README.rst | 69 +++++++++ .../__init__.py | 1 + .../__openerp__.py | 18 +++ .../models/__init__.py | 2 + .../models/account_bank_statement_import.py | 139 ++++++++++++++++++ .../models/account_journal.py | 10 ++ .../test_files/adyen_test.xlsx | Bin 0 -> 17972 bytes .../test_files/adyen_test_credit_fees.xlsx | Bin 0 -> 18800 bytes .../tests/__init__.py | 1 + .../tests/test_import_adyen.py | 55 +++++++ .../views/account_journal.xml | 15 ++ 11 files changed, 310 insertions(+) create mode 100644 account_bank_statement_import_adyen/README.rst create mode 100644 account_bank_statement_import_adyen/__init__.py create mode 100644 account_bank_statement_import_adyen/__openerp__.py create mode 100644 account_bank_statement_import_adyen/models/__init__.py create mode 100644 account_bank_statement_import_adyen/models/account_bank_statement_import.py create mode 100644 account_bank_statement_import_adyen/models/account_journal.py create mode 100644 account_bank_statement_import_adyen/test_files/adyen_test.xlsx create mode 100644 account_bank_statement_import_adyen/test_files/adyen_test_credit_fees.xlsx create mode 100644 account_bank_statement_import_adyen/tests/__init__.py create mode 100644 account_bank_statement_import_adyen/tests/test_import_adyen.py create mode 100644 account_bank_statement_import_adyen/views/account_journal.xml diff --git a/account_bank_statement_import_adyen/README.rst b/account_bank_statement_import_adyen/README.rst new file mode 100644 index 000000000..9b6baaba1 --- /dev/null +++ b/account_bank_statement_import_adyen/README.rst @@ -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 +`_. 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 `_. + +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. diff --git a/account_bank_statement_import_adyen/__init__.py b/account_bank_statement_import_adyen/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/account_bank_statement_import_adyen/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_bank_statement_import_adyen/__openerp__.py b/account_bank_statement_import_adyen/__openerp__.py new file mode 100644 index 000000000..6f47e7122 --- /dev/null +++ b/account_bank_statement_import_adyen/__openerp__.py @@ -0,0 +1,18 @@ +# 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/__init__.py b/account_bank_statement_import_adyen/models/__init__.py new file mode 100644 index 000000000..ba1f49342 --- /dev/null +++ b/account_bank_statement_import_adyen/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_bank_statement_import +from . import account_journal 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 new file mode 100644 index 000000000..aae90b714 --- /dev/null +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -0,0 +1,139 @@ +# 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) + + +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 diff --git a/account_bank_statement_import_adyen/models/account_journal.py b/account_bank_statement_import_adyen/models/account_journal.py new file mode 100644 index 000000000..d0c431fd8 --- /dev/null +++ b/account_bank_statement_import_adyen/models/account_journal.py @@ -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')) diff --git a/account_bank_statement_import_adyen/test_files/adyen_test.xlsx b/account_bank_statement_import_adyen/test_files/adyen_test.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3adaa8dbf10f9a2a48f2e2538a5f15bc86756b46 GIT binary patch literal 17972 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 E08#;WUH||9 literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..0443fceb3eae260220b96d33b06cf1e8032153ed GIT binary patch literal 18800 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 literal 0 HcmV?d00001 diff --git a/account_bank_statement_import_adyen/tests/__init__.py b/account_bank_statement_import_adyen/tests/__init__.py new file mode 100644 index 000000000..f53fb980e --- /dev/null +++ b/account_bank_statement_import_adyen/tests/__init__.py @@ -0,0 +1 @@ +from . import test_import_adyen diff --git a/account_bank_statement_import_adyen/tests/test_import_adyen.py b/account_bank_statement_import_adyen/tests/test_import_adyen.py new file mode 100644 index 000000000..d4e8a6843 --- /dev/null +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -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') diff --git a/account_bank_statement_import_adyen/views/account_journal.xml b/account_bank_statement_import_adyen/views/account_journal.xml new file mode 100644 index 000000000..2e6f2e6ba --- /dev/null +++ b/account_bank_statement_import_adyen/views/account_journal.xml @@ -0,0 +1,15 @@ + + + + + Add Adyen merchant account + account.journal + + + + + + + + + From 9e495b66f0de0ea237513a5836d570a9306793fd Mon Sep 17 00:00:00 2001 From: Martin Pishpecki Date: Wed, 13 May 2020 17:05:05 +0200 Subject: [PATCH 02/24] [MIG] 12.0 account_bank_statement_import_adyen, account_bank_statement_clearing_account --- .../README.rst | 82 +++++--------- .../__manifest__.py | 24 +++++ .../__openerp__.py | 18 ---- .../models/account_bank_statement_import.py | 102 +++++++++--------- .../models/account_journal.py | 12 ++- .../readme/CONFIGURE.rst | 3 + .../readme/CONTRIBUTORS.rst | 2 + .../readme/CREDITS.rst | 0 .../readme/DESCRIPTION.rst | 10 ++ .../readme/HISTORY.rst | 0 .../readme/INSTALL.rst | 0 .../readme/ROADMAP.rst | 0 .../readme/USAGE.rst | 3 + .../test_files/adyen_test.xlsx | Bin 17972 -> 17127 bytes .../test_files/adyen_test_credit_fees.xlsx | Bin 18800 -> 18956 bytes .../test_files/adyen_test_invalid.xls | Bin 0 -> 17137 bytes .../tests/test_import_adyen.py | 97 +++++++++++++---- .../views/account_journal.xml | 24 ++--- 18 files changed, 211 insertions(+), 166 deletions(-) create mode 100644 account_bank_statement_import_adyen/__manifest__.py delete mode 100644 account_bank_statement_import_adyen/__openerp__.py create mode 100644 account_bank_statement_import_adyen/readme/CONFIGURE.rst create mode 100644 account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst create mode 100644 account_bank_statement_import_adyen/readme/CREDITS.rst create mode 100644 account_bank_statement_import_adyen/readme/DESCRIPTION.rst create mode 100644 account_bank_statement_import_adyen/readme/HISTORY.rst create mode 100644 account_bank_statement_import_adyen/readme/INSTALL.rst create mode 100644 account_bank_statement_import_adyen/readme/ROADMAP.rst create mode 100644 account_bank_statement_import_adyen/readme/USAGE.rst create mode 100644 account_bank_statement_import_adyen/test_files/adyen_test_invalid.xls diff --git a/account_bank_statement_import_adyen/README.rst b/account_bank_statement_import_adyen/README.rst index 9b6baaba1..38929e877 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 -===== +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 +Automatic changelog generation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +`HISTORY.rst` can be auto generated using `towncrier `_. -Bug Tracker -=========== +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. -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. +Please refer to `towncrier` documentation to know more. -Credits -======= - -Images ------- - -* 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 000000000..4be97c2ef --- /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 6f47e7122..000000000 --- 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 aae90b714..a8d17f60f 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 @@ 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) + 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 @@ def balance(self, row): 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 @@ def import_adyen_xlsx(self, data_file): fees = 0.0 balance = 0.0 payout = 0.0 + statement_id = None with BytesIO() as buf: buf.write(data_file) @@ -94,22 +87,24 @@ def import_adyen_xlsx(self, data_file): 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 @@ def import_adyen_xlsx(self, data_file): '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 @@ def import_adyen_xlsx(self, data_file): 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 d0c431fd8..44a3e7f87 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 000000000..7df8a95ee --- /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 000000000..58e9f494c --- /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 000000000..e69de29bb 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 000000000..c0832e16e --- /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 000000000..e69de29bb 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 000000000..e69de29bb 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 000000000..e69de29bb 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 000000000..70545a9ff --- /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 3adaa8dbf10f9a2a48f2e2538a5f15bc86756b46..b7cd4c6c287f961d57c8cc88e1b1e37eb065f01f 100644 GIT binary patch delta 15595 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 literal 17972 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 E08#;WUH||9 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 0443fceb3eae260220b96d33b06cf1e8032153ed..c4a786de4730536e198b1d366cb18d4f28282ec4 100644 GIT binary patch delta 9945 zcmZWvbyS;8vrlj>?iNCDid%sKg#f{=xVyU)4^W^G9Ez2qr9g`oD5bc&6!$`lyIZjf z?|09+eZTwsk@IACGP^Usk?hVKCIMsK0-vZUBBKxkFfcFxAs%IFU!ITvQ668CL!>}( z%|7ROZv3VTc9fq^PmS%VQjv9P699|&MSU4bvZM|O;$An$qRp09P(3@u`H$C-;dUVx z>+Dpa4Sl~6)6ySo*1xz-X^RcXr1mV%_vmQ`(`^zsp>xYy+U_(_0pd!l0pWbDH+{8@vck#l#NT zjUGy3Zx9+p&fJB4RJT??RjS=$I-w_Xov$LwyWMK|W|DocqIlU%f}{&b@ekICwsMlB^ak-W8+m>FXi>eI=QtG9Oh%uG;OO0rg7-w{^8EEf1cr z_tFzK>*V0_ly+TD8EUK|mF$r1ucQL2EnB^NrT2dQxSkz}-=KXSI%-BmgaxhDmEh#g zX$}=Z&17#(q!;Lq}kOn>(~(TvDKwnu_P&tTE&{vO7kCJyZuI)IuYIz~O+ z4CNgV0PsKtJbeG#Sdy3N>EH$~bKJy1OBIz1(`A^`V_3x^Y=uoqlL|%h^R!C8A;y7G zj^i)jx74CxVTx~Ti`1X`x&;hh4PRZlSK@A&3{#-u>9T<@0fH)l7~FVJn2%WStK_2w zS`-BaS}>b1Z7Ztwq5EZF#cAm8c@zs=CA4xh?opAZLopIaI=vU027EAl zB^wWx7Ml5l!=IpOi%3`D-4hBbzA{OWwIQBq2{%wE00;M*GZj*rGek;y4S8J`jMY@u zmSum~H=j&&KXLI2`l@6U+ru};*X)u8(uZn6reFo*Viwtz3$3{H&gH|o-X;(O+4Igx zh2%;hQK+K3ik~7MjFuK@eXHKiw*{;IFz;e3qj(bm8wC4ObDOc@DP`5G7YGl8Ufq zWi8oO0zp_vsYe?;0Sr)*i101_o8O8nm8xz9$`tayAyi*se1cLAZG%IE$pmg0cqM?{ zW2<*uJOip<7wQKljlVS$Ct4IYW#8*TkEzCSfQ6@m$N{m4INLvk=VEh)li#z_hnRN? zZlGuqlHP%d~G4 zlQ1!HYlydoJ_vlgNl}TF_UiX7$JP=|OVRfp*r6XUh4<}(JPu!#Fli=#>YK2%=Y5l4 z_btw@$WbT`p?qCD!yg|tbS1&Gk`jNg&ut>olSAVdlOvZoI1rF6fBQ9S>s8+r$H3H# zN$ppR8CcVF#yK6}|u-lBNwjbMlgSi9E$P<`+?ChGt8 zI>28K10eYlXX8Hsdj9VvI2}}E9NrVKnsocEnMVyJ5G>aZmU*x7ebvd35 z@V(eyo?q*`hoCokJu@0Ej1I_X620Uzpu1VwY4{PWp7^PGuWHoGS8YF(H?Ku1+%x5 zZI28L+zisr1TIL3?805(SDUVa-=1hyoFaD57u&vE%pZX;*p#1se`lk*qjP^yc5qTe zWH8>-<=MnuDSr7SeMEdDE#tX9Y5e@_jjQqN0G^s#vxd7f^!t9q&6OliOT#y>oA-;U zBQmMu2V}G02G;7?ou%tPx}2qbT{(D5CtV)E>Rs*c9ox8=v^*+6@0t`?Nk_Uir$5oIk0_NQX`jdAYV@zUX{i-`vMIl zeU&7A<&FE|O0U?H^4Q0M>ZfW^oyc^w*)!1&lSt_4Hfv&}^Q{6T-((~b>baW^ zgN|eqMQzSug9!rt|9H%B z+0z4_Uq2$Bh+9JtwBk%0@6tGfs>#D83}Ry7$v4|ySxOz&n~fP)&7lx?0WjfrYJw=k z_(&)xj7t8cVrnMiK-l?BnDjEp*rf^~|K6GWw><(Md@I+lsa zgl}$D>f`l zNB%L+?e(&pHsnoY?_Q-M>LZFMIsekcMEHeQ>Bnc_hdPUx-`+8iQ1h@PNsh{gbvocu z5c79|F`3*7i+3Zuq;nPo5iLrvBLd+I#MTTzt6UYbxCM2>Z>*JA;MD+ouCD>zPKcngp#ur@!S zBw*-M$4YJui-KUvBo%lU@pvEtm7;d1XV>_$NmPNZ+T!Wsjl!iH+`Nl)hV1{J|62ua$fgUB)2CtFd+T4k2L(+hDdS zB%TY|K}Gj7+40I31hK0MW&+mQmhoWTDtUF1U5%Hh^Tbqk-&McxBsvm&W2y@x&6vS9 z%tqqO2iB<(p%QZDV=@{Rzo=_OtBcu)P~XjXnSJ?%6!16%!-j(~$&Qff#83^3fpv3b z-Ghj>a9%VrO?pG6aM#2{w7TAOfZ^s8O4=4B=ZD~ETL9+-^!R0h-RgJHD9Er7SkCPl zn^x$|F@mOt{V)eIEI>UDOYj96$^yyk0p;E$Jk{o|c`?{w9wiUx9v&ZLb!7Q&7VLa`%qqhULhEs+`*R;awBFS3U*q5bnTPK|# zM91NL%Tt$*^~;lcVE8X)ghr8LXlnbn9dmup+vAcwb!nw#GEUa z@(EC4&M~A1309k&r;vOV6H*usyjw=$j?7^(lmw5Qy$UPq!E3Eqs9np-={g-^2O<=$ znwH4ixPu-Oz1RH6g;8r_a=61D*@UL=x#Kh?l*K4q$*V|P#h4SFW}q-^%in}2p~_(> zyJkdoBRFchu^A-uP+dQOGgv7+6GQVClawr_AOSB^WmNGvZe#fgu9!?a5I+6@M3m-( z=!81$58{@v?tris`Eli^fDXXckG-O%PY)r6h(TmL1#n`8|cAGp&b_4aNbzr4^ejD^RIm_lbg9rqC? zLU;5B=#emW{8QnrWOx=AZr~GwNQ_PU3rbC{>BJ_#7oH&uT>G3+{h>sPjj%}?3hzd9 z3tXQ>bztK}2EOt1C;V(MS*RU=YXXwj8Ep%WA{*@_a$}r38rKv77eW8%$;6?pBvDgTu>Z9<{Ac9jWi%4HrfD^%g_SG0mQ}fYh5Vgz0sKK zsA`8|fCj~aJhJ)_B{r%msSr#V%HaIO2jP_x@LSkN_Ea+zzZJx<5#fZmJxdTy(B>NhS^6-d+9l{IJ}Ws@lAr zSS6b?a@cFN-&CXai(4}AU9udOxPzQ%hKnY72H(G=D&%Dt2iSE z!*REhJ}{9U<_}$DX-AmUsllOvO!{?cOl#| zt2t-Z$Yw@zcd_8(5q?Q>dytH~q>O8Ru@7#@uP<;#T}Ru^904FeGwjX|ds$4K;97{h zEOi=G1V#2?mAW5fbAzvT`@gU(XQ0b(Dq!<9zli-DAfNUd6~j>}nA2-{$&=<%2+=Ar z4lpV{vdkU}MyxWpK~XXUBHr9RXI4mt;55R-x%e%e0FA_X@Hvz#C2EqcB(4~LD!kBp z=$h7r$qm1&CfBLC51ht<6(HA0L?n6~>r6H%9-*^rNSLwH3tv7E%2%MilKhooep4|E z1A1;ZB8__)hp4ZTaN|n8p;T?Yb$JkJthkqhTdW#XnKl(f{X-R53_srb7 zvMBL~$TK@fRZg`>HSzqs5@zQ0j(+b6Y|K~Zn;@D(e~-Pimmb|;8H-(*Xow%noK_1Y z>C=3^&h#BVV~;^ro`bTRqFMT~4|Nnk_Rc#lJ!AEEY~e5QXQMDSVr>iS6w~F%UJQ~N zWC+Q-knBo(0ku)DxR8oo9#hI(B!TRJtL{2d%*lSH#t{$Lb)vG4kL z#W3-{?gap5wuNcp;f+tV*26yJ(V(wivOLNwi7Go2>#~|@r5c6Ps#ZXuCNz>rQTpK1 zy{1`!%y9Ec=FW$mbX%Z;vOBP;S7|(8fXfHW&DK-GBOITHxj$d-rtNcM?0aME0(6AV* z7szZ$E+eMS0Mroy3~EuyOr5*|Lu73>*SubVA2lAGgTdH1u|4Vbk}q4s!IQxmiTui7 z3&R9XNls$v=k!6!hMrlp%j{#w>2S0~Ie$wOX@Ngx1z%G@FE2P8 zEGjuZ_}~W|Ffpp~r*j_2k8pIAKh+hZ%To)m2)0gVF4e|W@}&83K>`dc1Tw*L4OG@1 zzzxRmKxXohY?))1TEhDvDZVOZ`4Nd25!_su#bAx5mY^0$EqqMHpAJ`Y&x_m?s_WAn z^2oO^&1De#^O0f)^NR!0haKU@ipUN=+~61dl{)yijwDcIU4EHg$kQlpZ^L*I+z$*% z5!{Ur>EI!2aC_oFYtVm@s90zy!341=h0tju=^TmoEX3%Ym4!WNgfB=!JPLu-uu|P- zH-`s<#zxcu@x@%>`=W`@ofJMxxMC|12s2ZnNR19Z$P4B!XP=(a%m+e<&D=GVq_QI* zyxk~UfW~|^oYOF^k`P2W!+bNiYKUJFHOjL87lj(ZLXZZu_O{z*y(vN`=F4NMvNRZf z0p^O7%I2Cz z5Mm7pKW0;6qn@wGGKQIU04&16w zwVc)=&9>&vHxg$HZ6$ppggmbrnJE1D*9ChQzESls3g<-IUhdAacP@rAUo5i?8=la- z%2~%=L-m@H7W91-8Yi{5x;h)Psz3212k4jfGj(RA3GP&*9!q2yrBpjp4&S$@HWr^E zlL%v(TYj7nk+ezW`iS~UBIP=7%EsMDC19%e1NSNps!MPFIfj;)H*>UK*aEZKBgtA9gu>-Rzb9 z)5@fmPR(UA zeR0C;zhAA#(-in?jmd!H=LR0$`NbX!$H~X%=XJv>mV2ve0lGsaH;(WND;(pvh1C*Y zfkNX+vdo_%pRS9AGM9eNx#_2RU2$p6(cFn-;s!N>f;`hltUKz1KeQ;*X~W?r_qX5R z66~bCu*==#&7|q*TWftY1^zU-<5M_;CvhgxGeY_WY7x&>F`SCul)OgX)lL!DHNdE8p zyy{|P(*Lcw^q@oFS$YPl3$o;a2Njj)^!9Y`vaT6{`Ihqe+Myt%l(97X!zZRzPA+@1 zEH~Fd&86}tpZQSf8M&9T5B!?>&igVC&dM)I8=CDi$4TIQ+A@y)-Ub4AMJ4l@crEo> zqeVX)JIbAYZ*8-lmk8NNWSs`G_10{u>cq3|nUqZjUXky>f3!GWe93EB_+~^yc>B#? z_u`r}!s(ve;`1wqwsgJeJbP`KS=x7=^%~h~Pn6Ft=$#9fT%T&&Fb8^h^R_o`U*thN z|GXFXUi!W5I6ig#LyW{_;D}FUYHQc>`TKjk%;=ev_&j+%S)JSFy10ukA8IB zF2YGZ@zMGCdm}#6#ND;NZs?2OePHpbG)MU;ib?ldvLRFM9(PPX0rSDcTtR*=uSq=3 zxOu~{{T;LToO>~DNe=POnHAX<0lRl({k)E+2YRe`7)EdsHNwj;I`_@j7}0?sqwd*k)xS< z*Xf_(>+F{BL~vBpE&o_TNY(56DLtzUGD9g@KQ;m)-KcFzW3Q*O0X!dR(*4VN$FF-^eL0 z4UWZYkPrN^kT7MqE8CWq4MDeQ^qRxI9RKzjt9O|NuE0c(VSVx^$KoloHO^3RczT<= zNSOLQ(iTpoK=%tlmG2?;6eH}^wH-C@dBohpNp(RUa?dsdh<;kYqAhfzcThVN*&$Ob z3$Y!YWg2r7At^y_I2=u4=DJSLg;neY-Q(PT`53b@%Bu0bB8+gCV#Bd;kMY=xxOHsh zry~>TfGbG2Kt>9ZltN;~eAmgpHm6cjuP|Ea>J`2vKubd$R@z>Y$F4v^Y+t#SW|8%9 zi)Ym@;W{T`xtZ!&_uy#Gxu(gT5^DCEdk?yfpob^6^%@iQkjoZie06y?!!`do+R9I2 z^YdrlR(;0;Y@uCab~BpFVm*#a^`r0xrXB|C7&ydvlAPfwZHRRHh8u+3JRl)H=S8LU z%M;sYW#PY;#l&9+Z}h;z@`8#4c6rB9%X`&&4k+?X54$z=Uzn$}@24~Qu0I#1a;J!+ z7Rd|MEb)kl^)s@zEvE=}SDZz)>O6s%J!K@tw|TaqOU%dWSN{I=Nft{(-J^ewU$05mmTBaoByLA7GwEv>a})&7Nc#LP}`}`OT8oMpbPrN$mxx^OtCD=#Ol5 zP!vc+BDU*Uz>1hOxAgX$9u!R=t@i5P5gQ}Hffxye{J6u1WS)?|17rr`h+_Yy5854rxZp}oA7Lhb8b=wSZLb@-Lts5;t|q;xKrV=f za=KOxk>mP0N=1PEx^YD(chq+Qpw9cFCf;id9x@-RVG z7*jk-SCTlD^A<;HEavr~@RKxcPlA>1*Lba|=+>kwDRx@8piV-pfWl+?P?w$hA2O$T z)JqOA6{bNF{9njhA}=ccP%phP3@cxBhegE3?9Pcrv^7uSOqd^4#bZP%z zObuqdrT$M@q#fn_KQE1_Z191{dUoRlJH}6D0r{Z}iF_p3W8;H9A|L^nazmbJ-b*3a` zBwELN{tTGe8v35e$yNTmB#bB7liF15ZrqggfV=o}_ts9?(H?FN!%N+65dHdJIOMU` z{m~nbXe5PIaI(%W;l*~GLmKxwi!-$;NO9UXW2Wtp-;zhc)rVQT&&zuJBT53%K4!;k zmNNY4JzQl+tVC|U9~UVpg6V1%q1T3CpMP-LaSEk{7e~gFR!{pySk##&E**TERfr|o zo#LFC6ue##4D1!^g6}WIL^satihCH}_^}qY8mef#U)a68b$5pfl61-N8m@FV+6SQ} zX^aJvT^en2@E4YAslOhYh+S<+x{ZG6!zkX}nUcE%e7fL}Ec+ERz7g+9=q72_SN`k( zI!51TH7Y$qK|#DP`RDhmXuPl0N8RS$$`D3KqEz`N9S9|o_8Lm8cGZ#-&9IG&(lv)p zLeOT2BPAJzxt0RbHPYTmk^|J7)DBrsfKz`6Y&guzQh8+l-2#uxb{O~juxKIxyW&ndc6iKaH;UtWs< zHPpWE4aV6tS&&sX*@x2TG8-5rK@jqQci*J8>me>BGQx}@(uJrF`nZhGf*Go}%a@rp z%EHO!DJxmp9_pJ7?=zDe@s(9Rtx%~A${6PbdMxU9{g5_*Yj#}Xm4pU`E88 zN90k^^z~^Rj*dAx@sVSw%kpAo7&I}d6KjK z+Opu!D%H9xd~3<}>5D&noQLeIJ>er|7rVdQzJjzUi`>YU-pJP~F!_oM zt?X9Q`8jbb)mA3o>Kxfbd}7$I*sC>JYCWnO#|Vfeq!J$6I9|H{6a;+KyTO>O`^yKt zM)3XJ-2&t&hI3 zS5lj>x-kEwI-r5iu|Ze!J&0*k=uoGDr0 ze%dHB{kFal&__LrMyzc;r?Bp3w=q!}(6%m#-t?1k=<9KAz%(2^&hzbJd(2bCoCPOW z|Dxza{_nGAsGrRGV_0Q|#Zv8x$MoNaj|l*D5OBBYviAhgZdx6T70w`SZu>hK$zE=~ znF_f^isCn&NanOW&g{YvBI{e^1Jn8&Vwy`Lb_GuJFG zMyW_XQu0@*!D(>DC=+mon%rsDQtI-Yc6>ogx)_pSijU#x>l8j~lV_I`&O8y%?+NG{ zj__35Gmu=hKH?Cv*&C z#;Ux?2dK!=AK?Nb5d;3h3n(i9dhCAsyXOV(ZvzKV0FyD~Y5pPlyF`!sFQS*o$CMh45WcU}+Pqc^HJ!3Kj4Cf!hzYhrH|00}5Pd>t>Og8ynLe;}7zp)1b z#s^QN{1;&&Rr0tj%Rk)yPL0I>A}XOyu95@)CnbJ{Q6gc1lj9gklNVq({~5cNr9{G( zNdAGsm@FyxpRN5JbN{_HRLNvVIVO`wmHizc2>!Jqh70+Vg?&9jfW#Qs(#p&bZ p_}{Ak+l%o3_47>WWNlemy2myC#_r)P_1Dvx43%d>iT*v-V<(%c8+yk>HD4a0|{t(BKjv1Pe}(1p*0f3k0_iAi*uTdvFgBU~z{4 z!R>?hy>)MX_x|xsRi8S0YWk_Jex`e7r#mtM>32L59#jPdl^B45fdR-ffh$+wkszTy zzQDiA(!)l@-*j>l01?ft9ihcZROXSGm9i?^#J1?QZhYU|)U}K8^IV|TSE@pyMA^==ob(Q)QUs< zB3ewf@fL6E^qG4B*0?#Ud8$8-xHkhdN_A476mO| zkO^U19$aH$84R%-I?#~v?Z#t!>|%P{YTctjRnRettBI}G004j;5&-a*KTsb1Kq>=< z#f_?gfoNpJbB?5lGw}gG-Z7@QFN!$Dnt-+mR#JC z_D>22nQ~%Fh+ua82-w`B?jU`GKB=@FmJ~s|2;e&QjTS)V@V=h0uE`~hkKhyXEu(2$^de_P%NK8b|nx9BJ>>ehE9Qp`taS2H85R~)oix+-1a>4OtZlBy^3 zB&=KZ3K1(l<;GY)%L&n+3E0QSU1Md}o7jnDVQWHJjb`BIk8^8 zdRD{kc(;ZzO=*g#$aBCjZcirTv^_YqM+eUKhz_C9EQG+OqL-is>`6@hNteS&Wq#NM z>I||^#qVhCQdRh7?sbH|zs?JQle7vXbB7B<#Sk36?9VicmNS>FTLZLW6so5|g|EE+ zFm=C`bNlf+eae!t_}e%`S||DS82=QPbc`n(7-e^_Bo*;wSbq)wqw#mBESljwTH{Y^ z+WGu6U-&QBecF-W(!rsmOXjU8Yfqceh*tW-+QscV54mZkZh2m#)gG}o{z&m!TJOm& zj9Cn|Ea}sN^+d~*Jv6kI#V-hm^aA|RCf!He!})9&XuN5uP{(@8+vCXy0&y`9Q4Ljk z;D@;+kgJ~UiymV-Kc%V#iFwOiCnjnI+O&1FKY^U zT9Fb?xxrqDLb_?|<<}w+rj8$71r-%#aZwnC6jnu=*i3IVHN<{<+ZIiwmtPp_!X5$! z9pjAPO?dYM@3ScX5kQaeQ%}VR3(*^JoL3-TIA_P;4DuMWU5ruCMf8QmA^W%0VCSV- zs77&u!C&JUI`6I*=~+dm*6VniV!1~AGJUM;E-s85tDY`Z%Nu75Zu#&I_$JQm=Drh4 zSy=GDoEoeYth3wy?!K;J4Qa{nGrFs`osUJTw$rdqBk*V)+H`0+`toK8HuKCha6QLS znGEsfMA*qQ%5&Fa)sLmtRUtI>Q)!{KfyrT!GcOTo?q1PKRCCdfg|+TEgGY})f)6v8;37^t--mRsnO~abm)P93V9bte7MT)m z^^_6e`}#)@dHtCkk`wqO!N~A@p5UCYaX(ge5|eMRmSaw_P3a`$P0OJtJYN#L?t9jK z?J<5=gLUhhlgD}uvq`(%PVG=5*y|m;ku;a6k9DdX@bf-Qvz(7|g87}S&46FUvc6X) zcl%+c1x(|Gy4YWOxO-MLG}K8n-<*BeKAf1^**Q2qy8y8jLOX0b>vdDJ=L(q?K2i|Z z>HE~m%ZbR+n_*qs?T?vgk;@w9B#@*NY9DVlruD~0HI^M3pWgm_BDS?qS};(dw)I4X zl9Rt8^4PL*uZR&x2WBAZJU}0Bu^+lvEI3}t zIw)#4&d*8j+QyR$KgliZw3kvwc3{LJA&}iLM1T3gXF5)f9A>`4OpsbkyVYSMLwhytvDdCTKIm_%{Bie#%p5PC<^gag(DO#6^Em|ZZn||P;1*t zN-B`MKC%>6XPs2vBHx#BIb+Sb{P!CKlZECnoXbH(fzKx!1yMJi)^|0L!xh%glu+Vb zkW8RfbMk`3ZZJX*c&>C*=9tAF2L3S!OGf)TUM2}hOA~G+586Lh<0cG7P0X{OCXbir9i!7!7tHSDPg6Yk#Nl2E$pK* zx)$Jx2mawGJMpOr&I=(eUU?pNlboNE@~^Ny0A(!4F=3YV&R<}e((UF~A?a8@urkpe$X7#1bi!b9~G zt%X~@L_q@ylggmbSK;1EL&jLbOJ6H5xWr800u~mn2oRPP(S;)h}05xQ2_u1=)aO7=6@fr z;CsN+fA_z2bWYgI%8;dzvfsaDB{LYRlMWg&#kab;o~dI1kq`V`%KU z@em_Bb$PVLc`-&hW}TNhyA$!+Oy2iS_bqAe{%h+lF9(<0qc%OCUEX|4R^1djY*-qO zxr6L`Bu#lqkdK6W-<4kbwCvsOBVcE=;m5V>4w-t?HfFFE-jJZpWbwtMG%%KPn# zpgpYFv%D2nd%61lwtv#Q@bft9$*$*u?8$&R1n13=vJ1};ZPG}~p_u9>d}TS0Dyjy3zUWzqAto_JXKEXFzFKkRmV<>-Y z50%Vcj$FE*+E!A)BnNDUy(PTwJWUUaQe0imt{2W>7H76*@uo3{HagAN!^WD9wfD}l z-ZL39pezx1cdo~gMApy_hTAi}nln8cNxa4k$?%nfJv%YiBfggVz0K9Btbr2uJjoas z{choMN z3gAk238#%mtr^tk_iim$FoRnY^lvoBZdwa1yDuTios+4f`I;6T*^B1Q>t>d-gagbc zV9KZLRW(O526#eAveqI;|*y%@9}Gj6t+elBKoC**FL0V%P$Av@H@WnjlKGD497L#V`teVt{-kg7M z$e0Bff+iRUg~>C@hWd6u&sl*FGOYkYm?zDtB~PCsqRI-$L^uVLpV6l$9@_LFHWZa#1j zyzy&pmvHog2>HfRzH{k}`}O;6GriZZQBx=Vj~!7rZ!eZLKD0_O++O|`qMU-Q-uu+e zfo!I4-}){8S#4Wdy4HMZ`^;ZE`BON4VL60ETpJZa^1KkkluaTR{O;@4XR!a*3lfrk zmF}n7c@Pp5Z7T>#mQt4-w3Q$%bcZJ{wA`PG5Hv#s1_e?RhK262#)Xb%g2F=gMdCsc z{tSem1#I4VIX6g{oSdf|BN*0jPxtT*_AvEenSLzvHw__HN$4T{@@P1KmX}aI7#}m@ zjI|4br!Y2xr!cK@Khc|OTVr!~@;O-S2`EgA;z6h$gyuo$N|=t6c};06?bpU$3BtL7?04 zj8A-7btJkHps;dYy6CQi8^TV;*le25a_YAplCmpLPrL3ibNbG$Y==|&$V4uCh_%@lYx=A35+iOpeha)lW5*`^U}HG5rYAmDMJb zf*hEPAVChSjA9|rMTe=1O!*Y7-pdkCSK}i(=G8#=p0Jc_9xf=Ohs*bD;b>5O-bOI4 zFC{8oG4s()0$@UKyM*zS{l;CrwBSHmc^7MV2J90bB~!UntT-GS^(`Y=C^54%2V*Zw z+)2a2NbH4@n75dOY~o@6tK0N2-?%v^RYnL9qKF}1%B2Ke!wM{=5dPsYs5w0hic^e+ z684h>DXw8jGsb_8bg>eX9PBv#gb3YhTEk#!1I_YPc;JRI^lJj)Z3hOK|4o9J_u&XumSM={deGK9bK2af<}9}r>xKpqFj zULw~O|NkKM%HPx>qZ!GeIOi}QF&W8OG8z?1*99@x#!kraA_rg#V33gqCOU&m z0eaaFzW{oTMf9%S2pRJC5nl-&tyIiM;)*8l)}h5W_)W zZ-$K`^UT8r@-~(Rq-5ZRh~1iBD$N9=OXg?bric?}xk&<}LF^X=QmQL^RPGU_SCph6 zUgX(2SeI`*SO)8?Jha*q$YO+>!u~z9{0v|umyAUQ%I}a5wxS>f#S`hvP3{A&XKsNC zW6o{-05ZFH)sB$V$xn|Y+3kpiGDr@9$KBf$0M4vect0^BgH^`7&B~ploomi%{y_zj z?NUh9*`gq~$v-0iaK*n4P#JqK)g%CGui527{cJ01M)BaSN}MrE3WG8xir}KJWc2C| zguTey=_>0o(BpA7shqeh|B6v~|B1yTbat>D5yIW{-K zc&Z;lj0BO}RK^H%o~#F4A~z`xyw7UinOc03<1CmrJdQe@{(|k#nxip`mfKgw)6lv% zxfU}~xC&#lZTwPbc2+|zz5FS8K^H;tA8UnB52RA_3<$_YUR)2dL33=Kp^+|azDOi_ zg7|OLeInj>OvToRiyM4-#>Be^V`-+tX0C(gS@Gm` z6)MICRKhH~?>N-!4YI||EvoLA#shDtcD&xJ$*Q@$?~`psB_Zwn9esV^WmC zIku>XVH^V%{Y#n@7#&w&*qK~RXe~3dhjB}7__6o|fT*zF$_ZzpZ}CZfT+F!s%NC&u zn<}?k)l@4P&E}^RR>%U5@03`G*L_r!hbuc2`2&J;eMgMsGmF(S2vZvQ;V}uUm9G8B zm{Nr*2Ocv_o<%J6e7Jx>iq*~BL)U4;mV}SFe#eCgov`ySc48#J4Fx z4B4oob#V)}c%iT`49wM6_t}nvIF{3=gFS#?)*pBF7{~GwC>uB!3^jH#`abIq=!%06 zf8*C_F9fPUPhGmn>>tn!KkX!bN;xx#iRQ`jl*u4}2niNw6=X(@)gsq)npO zKn#&G6G3X240*7(tkaJ+1|Um)iF1Igr~R1niiokF%PACQBgJal5{g?iPDyo_`rshT#~@7rND6t5*NYxJV+;chQbOAs zab!j~N#C-?1o>GL#zL!~*aAh&h5?wxWdo^eJYxXDa3IP?IDO6zBpWG|kuaPiCbX5A z(PP1!fJGE185k=S759Ai!MLZh$wE>==TC$gRbz`kVJU^=_6OL;Vs#6n50XOzNjxa^ zFRf=S!6@9s4#3H@BP;Y>*h5tF>Jyk%KpB1HZUo{gb|v0RjoCb8H6UyIz>LXxp`yu4 zZYL=qOf!I?Gv>2CP0Q6Fqgl=t-zbhtTRIYH-f;8`i5!|Xizsd=xrec|6%H0Da)3bjb4T@v`ux@p=HZ{%|N7=A1`0U*yyW|ds1crdpD#tD|;we_C2G9k{l zK5Z-aEewuA>VqcKZWF9o8V-`jN@ES5TJ94@ogIJgn2$~A-%;UF0`3rMNZk16D2_2< zGdt&SIeu(>knk13i6Y zvijV(!1sML>u0Idb{V zOt=_i2rnPqE}UPDs5O^}LbwXxJ7?Ry`!^nvGuEw1>`U+!bASJ|k=6!`IwezeV?d94 z!je0gya6n6_4U;LFUSd8^v?T|zLjQa^Qms5exU7fpDbXgX(;fGDt zdG|sF-~OGm$>2$1KSTX0uL>>cM1%W0OnGD?smOw-WWr&Gy9C9%8q3s%x4FfGHC>PW zTBEUg;l`bJ`_?P>-mEoQ^@<$@9Y(&yd3WW1dI&SUb2rt7iDxaY4-CJ(8x`R^=sCF; zA98xQUvjw}NF4q-1^amIqhW0qv76zY)-B4MF?IfMn?h-L?K9HrU49sJgJ9W8XjNVC z<%DGvTU4Aw6Ix5!L@r>DB@xgmpK&b_0MHQkze^$yg~opd?D2{FAYS$mS2u6_SFT)M z4)*&7#!eI5#D1H4`itxw+nW-Tqz?O7iuS2nQgn!Rwe&8;&$xY=M$;JTg4gk-qwcb9 zVDAdQ!_)ewy|v$K|CkJ0V)>iVW7_!r6n3L;8(ftzH>YiBC2tk}AjQ_@D<{(FSJy_^ zLM67V1gpvRA~Y`!P%x{+XZRBTqBqVY*pd<7J1FH0)u>BxI|qY01tZe;HFNjm>juY} zFp#3b%*;(#KW41uwZrVSV1 z=G%k`9b-YysVjZCGkQ;wduh4KXf7cB3z#Smkp$3Np5Hjbho<;&hl6^nvsYVdx<+vp zz=UZWS&yXTiStHSLsmJTPg#a^RT)u(!17@eU(-6t{+JX7UIgx zwn7f~^Tr^~Eb=s>kH{6dC1$A~>oI!Mal>D6)~`r#cKabl>0p|dNd#A_{+#++1r`rF zI-Qis4B-$~DuZz3r>iRvH>Lj((wzOlk0i=UDfz1`nBy z^~;e@y0h`i25YHbLLEgJv_VNxO(~NQ7e|K>Bb{XW7|Uw8*KW$(B|aSiBL_27-wcCx z7E$b&QLPuj&qdkjYYt*>aT;D7~Pdp6px=uVA|6Qny;r*uA5yT%gT0kgCS+s(Ot9RCDzN;<&9gd za5)5S9YV98O!e6A#8EuBu1xkPw*MhKo>Q}xEj-U~k8VgNCNqw?3oo>@j$Mlu22!;H zeRb__;vkbppB^CUWV^$EZc9=0mhw-Itj5#$P`uEog6T6Q$LT2d3ol;A?88>6-paD} z*9P_`uV4|mz-GxmLPjN4C|vZE4xPVi?!r=kf`p%jAa(3iHYF?H)w+20RJ{JXUCggbu!#pi>tWH~FuirxPp#`j$H&)G<@8g?tKM9@Fn`RZ~ zB$3r!ls6^2)Y^J-vep-KZE6urF7-;Y))z`BkNr$$5$fk?Q;+Vu?Fq#taiUt!P%HTD zooz_>B*bN7*m>zjB{cA}+INp)p;8W%qv1imCAki30gWX>h0<0$^bzE5@k&f`_7dow z#97Jvk*%d0R8C`{@-71d-4u`1vkjp~CL8Ga^V1%R&_9s>HUSk)A4d{t@bt%&zM6C4ssF6XSF zgF)PHDEkfC?zXfd#9z+En!H$GhHYVTj$s?RvKy8L{SwwsS-)v-cdnf!m#8%8uA%-zh(Bx zZ@lB#XPVNo6qF-kqu{K;KhsD=EkF$6dfG*CzpgK}4f}X=hWW14SI}|E!}w@*-JL zNcZZ2mnXnSUXYN90sr(H{o8N^&rlG6JvJQu-Gf8$H(>zs2VE;COQ?&Jvn#~h+4*so z{0rxwzNNpLPaeu{e~o7ktKq-3EuCCm{adwG-J{8WcRf7$52aExIK31FTv!R`pNW501d0D+Vm&%sNfG=ny83ep z&yXXI+|9yb?PxL8Vc;j>Ce~|yqUXMqK{~6=-aL9AYf03OY>~>*-M<{}g9_{`+ vQ1Jgp6i7~M~!OiUNyJyeY=brC(>-{II)2q6l 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 Qfc^Yge71%;^8FtDA4h+JegFUf literal 0 HcmV?d00001 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 d4e8a6843..acbadfdf9 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) - - -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({ +# © 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(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 @@ def test_import_adyen(self): 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 2e6f2e6ba..02f36f051 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 + + + + - - - + + + From bc9b1db6afb947c89f8bcb64c69eecf4cdf37dc6 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Tue, 28 Feb 2017 18:45:32 +0100 Subject: [PATCH 03/24] [ADD] Adyen statement import --- .../__init__.py | 1 + .../__openerp__.py | 14 ++++ .../models/__init__.py | 1 + .../models/account_bank_statement.py | 80 +++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 account_bank_statement_clearing_account/__init__.py create mode 100644 account_bank_statement_clearing_account/__openerp__.py create mode 100644 account_bank_statement_clearing_account/models/__init__.py create mode 100644 account_bank_statement_clearing_account/models/account_bank_statement.py diff --git a/account_bank_statement_clearing_account/__init__.py b/account_bank_statement_clearing_account/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/account_bank_statement_clearing_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_bank_statement_clearing_account/__openerp__.py b/account_bank_statement_clearing_account/__openerp__.py new file mode 100644 index 000000000..f69fe697a --- /dev/null +++ b/account_bank_statement_clearing_account/__openerp__.py @@ -0,0 +1,14 @@ +# coding: utf-8 +# © 2017 Opener BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + 'name': 'Reconcile entries from pseudo bank statements', + 'version': '8.0.1.0.0', + 'author': 'Opener B.V.', + 'category': 'Banking addons', + 'website': 'https://opener.am', + 'depends': [ + 'account_cancel', + ], + 'installable': True, +} diff --git a/account_bank_statement_clearing_account/models/__init__.py b/account_bank_statement_clearing_account/models/__init__.py new file mode 100644 index 000000000..0882dd26a --- /dev/null +++ b/account_bank_statement_clearing_account/models/__init__.py @@ -0,0 +1 @@ +from . import account_bank_statement diff --git a/account_bank_statement_clearing_account/models/account_bank_statement.py b/account_bank_statement_clearing_account/models/account_bank_statement.py new file mode 100644 index 000000000..06ee1e16d --- /dev/null +++ b/account_bank_statement_clearing_account/models/account_bank_statement.py @@ -0,0 +1,80 @@ +# coding: utf-8 +from openerp import api, models + + +class BankStatement(models.Model): + _inherit = 'account.bank.statement' + + @api.multi + def get_reconcile_clearing_account_lines(self): + if (self.journal_id.default_debit_account_id != + self.journal_id.default_credit_account_id or + not self.journal_id.default_debit_account_id.reconcile): + return False + account = self.journal_id.default_debit_account_id + currency = self.journal_id.currency or self.company_id.currency_id + + def get_bank_line(st_line): + for line in st_line.journal_entry_id.line_id: + if st_line.amount > 0: + compare_amount = st_line.amount + field = 'debit' + else: + compare_amount = -st_line.amount + field = 'credit' + if (line[field] and + not currency.compare_amounts( + line[field], compare_amount) and + line.account_id == account): + return line + return False + + move_lines = self.env['account.move.line'] + for st_line in self.line_ids: + bank_line = get_bank_line(st_line) + if not bank_line: + return False + move_lines += bank_line + balance = sum(line.debit - line.credit for line in move_lines) + if not currency.is_zero(balance): + return False + return move_lines + + @api.multi + def reconcile_clearing_account(self): + self.ensure_one() + lines = self.get_reconcile_clearing_account_lines() + if not lines: + return False + if any(line.reconcile_id or line.reconcile_partial_id + for line in lines): + return False + lines.reconcile_partial() + + @api.multi + def unreconcile_clearing_account(self): + self.ensure_one() + lines = self.get_reconcile_clearing_account_lines() + if not lines: + return False + reconciliation = lines[0].reconcile_id + if reconciliation and all( + line.reconcile_id == reconciliation + for line in lines) and all( + line in lines + for line in reconciliation.line_id): + reconciliation.unlink() + + @api.multi + def button_draft(self): + res = super(BankStatement, self).button_draft() + for statement in self: + statement.unreconcile_clearing_account() + return res + + @api.multi + def button_confirm_bank(self): + res = super(BankStatement, self).button_confirm_bank() + for statement in self: + statement.reconcile_clearing_account() + return res From d34aea00586fd7c18f32f640873e4096968331f7 Mon Sep 17 00:00:00 2001 From: Martin Pishpecki Date: Wed, 13 May 2020 17:05:05 +0200 Subject: [PATCH 04/24] [MIG] 12.0 account_bank_statement_import_adyen, account_bank_statement_clearing_account --- .../README.rst | 35 +++++++++++ .../__manifest__.py | 15 +++++ .../__openerp__.py | 14 ----- .../models/account_bank_statement.py | 61 +++++++++++-------- .../readme/CONFIGURE.rst | 4 ++ .../readme/CONTRIBUTORS.rst | 2 + .../readme/CREDITS.rst | 0 .../readme/DESCRIPTION.rst | 20 ++++++ .../readme/HISTORY.rst | 0 .../readme/INSTALL.rst | 0 .../readme/ROADMAP.rst | 0 .../readme/USAGE.rst | 0 12 files changed, 111 insertions(+), 40 deletions(-) create mode 100644 account_bank_statement_clearing_account/README.rst create mode 100644 account_bank_statement_clearing_account/__manifest__.py delete mode 100644 account_bank_statement_clearing_account/__openerp__.py create mode 100644 account_bank_statement_clearing_account/readme/CONFIGURE.rst create mode 100644 account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst create mode 100644 account_bank_statement_clearing_account/readme/CREDITS.rst create mode 100644 account_bank_statement_clearing_account/readme/DESCRIPTION.rst create mode 100644 account_bank_statement_clearing_account/readme/HISTORY.rst create mode 100644 account_bank_statement_clearing_account/readme/INSTALL.rst create mode 100644 account_bank_statement_clearing_account/readme/ROADMAP.rst create mode 100644 account_bank_statement_clearing_account/readme/USAGE.rst diff --git a/account_bank_statement_clearing_account/README.rst b/account_bank_statement_clearing_account/README.rst new file mode 100644 index 000000000..38929e877 --- /dev/null +++ b/account_bank_statement_clearing_account/README.rst @@ -0,0 +1,35 @@ +**This file is going to be generated by oca-gen-addon-readme.** + +*Manual changes will be overwritten.* + +Please provide content in the ``readme`` directory: + +* **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) + +Content of this README will also be drawn from the addon manifest, +from keys such as name, authors, maintainers, development_status, +and license. + +A good, one sentence summary in the manifest is also highly recommended. + + +Automatic changelog generation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`HISTORY.rst` can be auto generated using `towncrier `_. + +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. + +Please refer to `towncrier` documentation to know more. + +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_clearing_account/__manifest__.py b/account_bank_statement_clearing_account/__manifest__.py new file mode 100644 index 000000000..04d914fc7 --- /dev/null +++ b/account_bank_statement_clearing_account/__manifest__.py @@ -0,0 +1,15 @@ +# © 2017 Opener BV () +# © 2020 Vanmoof BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Reconcile entries from pseudo bank statements", + "version": "12.0.1.0.0", + "author": "Opener B.V., Vanmoof BV, Odoo Community Association (OCA)", + "category": "Banking addons", + "website": "https://opener.am", + "license": "AGPL-3", + "depends": [ + "account_cancel", + ], + "installable": True, +} diff --git a/account_bank_statement_clearing_account/__openerp__.py b/account_bank_statement_clearing_account/__openerp__.py deleted file mode 100644 index f69fe697a..000000000 --- a/account_bank_statement_clearing_account/__openerp__.py +++ /dev/null @@ -1,14 +0,0 @@ -# coding: utf-8 -# © 2017 Opener BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -{ - 'name': 'Reconcile entries from pseudo bank statements', - 'version': '8.0.1.0.0', - 'author': 'Opener B.V.', - 'category': 'Banking addons', - 'website': 'https://opener.am', - 'depends': [ - 'account_cancel', - ], - 'installable': True, -} diff --git a/account_bank_statement_clearing_account/models/account_bank_statement.py b/account_bank_statement_clearing_account/models/account_bank_statement.py index 06ee1e16d..eb30d3738 100644 --- a/account_bank_statement_clearing_account/models/account_bank_statement.py +++ b/account_bank_statement_clearing_account/models/account_bank_statement.py @@ -1,5 +1,7 @@ -# coding: utf-8 -from openerp import api, models +# © 2017 Opener BV () +# © 2020 Vanmoof BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, models class BankStatement(models.Model): @@ -7,25 +9,26 @@ class BankStatement(models.Model): @api.multi def get_reconcile_clearing_account_lines(self): + """ If this statement qualifies for clearing account reconciliation, + return the relevant lines to (un)reconcile. This is the case if the + default journal account is reconcilable, each statement line has a + counterpart line on this account for the full amount and the sum of + the counterpart lines is zero. + """ + self.ensure_one() if (self.journal_id.default_debit_account_id != self.journal_id.default_credit_account_id or not self.journal_id.default_debit_account_id.reconcile): return False account = self.journal_id.default_debit_account_id - currency = self.journal_id.currency or self.company_id.currency_id + currency = self.journal_id.currency_id or self.company_id.currency_id def get_bank_line(st_line): - for line in st_line.journal_entry_id.line_id: - if st_line.amount > 0: - compare_amount = st_line.amount - field = 'debit' - else: - compare_amount = -st_line.amount - field = 'credit' - if (line[field] and - not currency.compare_amounts( - line[field], compare_amount) and - line.account_id == account): + for line in st_line.journal_entry_ids: + field = 'debit' if st_line.amount > 0 else 'credit' + if (line.account_id == account and + not currency.compare_amounts( + line[field], abs(st_line.amount))): return line return False @@ -42,31 +45,35 @@ def get_bank_line(st_line): @api.multi def reconcile_clearing_account(self): + """ If applicable, reconcile the clearing account lines in case + all lines are still unreconciled. """ self.ensure_one() lines = self.get_reconcile_clearing_account_lines() - if not lines: - return False - if any(line.reconcile_id or line.reconcile_partial_id - for line in lines): + if not lines or any( + li.matched_debit_ids or li.matched_credit_ids + for li in lines): return False - lines.reconcile_partial() + lines.reconcile() + return True @api.multi def unreconcile_clearing_account(self): + """ If applicable, unreconcile the clearing account lines + if still fully reconciled with each other. """ self.ensure_one() lines = self.get_reconcile_clearing_account_lines() if not lines: return False - reconciliation = lines[0].reconcile_id - if reconciliation and all( - line.reconcile_id == reconciliation - for line in lines) and all( - line in lines - for line in reconciliation.line_id): - reconciliation.unlink() + reconciliation = lines[0].full_reconcile_id + if reconciliation and lines == reconciliation.reconciled_line_ids: + lines.remove_move_reconcile() + return True + return False @api.multi def button_draft(self): + """ When setting the statement back to draft, unreconcile the + reconciliation on the clearing account """ res = super(BankStatement, self).button_draft() for statement in self: statement.unreconcile_clearing_account() @@ -74,6 +81,8 @@ def button_draft(self): @api.multi def button_confirm_bank(self): + """ When confirming the statement, trigger the reconciliation of + the lines on the clearing account (if applicable) """ res = super(BankStatement, self).button_confirm_bank() for statement in self: statement.reconcile_clearing_account() diff --git a/account_bank_statement_clearing_account/readme/CONFIGURE.rst b/account_bank_statement_clearing_account/readme/CONFIGURE.rst new file mode 100644 index 000000000..b29c2ce68 --- /dev/null +++ b/account_bank_statement_clearing_account/readme/CONFIGURE.rst @@ -0,0 +1,4 @@ +In order to enable the reconcilation of the counterparts of zero-balance +statement files from payment providers, you need to make sure that the journal +that is used for these statements have the same default debit account as their +default credit account, and this account is configured for reconciliation. diff --git a/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst b/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..58e9f494c --- /dev/null +++ b/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Stefan Rijnhart (https://opener.amsterdam) +* Martin Pishpecki (https://www.vanmoof.com) diff --git a/account_bank_statement_clearing_account/readme/CREDITS.rst b/account_bank_statement_clearing_account/readme/CREDITS.rst new file mode 100644 index 000000000..e69de29bb diff --git a/account_bank_statement_clearing_account/readme/DESCRIPTION.rst b/account_bank_statement_clearing_account/readme/DESCRIPTION.rst new file mode 100644 index 000000000..7043715ff --- /dev/null +++ b/account_bank_statement_clearing_account/readme/DESCRIPTION.rst @@ -0,0 +1,20 @@ +This is a technical modules that you can use to improve the processing of +statement files from payment providers. These statements usually consist +of lines that to be reconciled by customer debts, offset by lines that are +to be reconciled by the imbursements from the payment provider, corrected +for customer credits and the costs of the payment provider. Typically, the +balance of such a statement is zero. Effectively, the counterpart of each +statement line is made on a clearing account and you should keep track of +the balance of the clearing account to see if the payment provider still owes +you money. You can keep track of the account by reconciling each entry on it. + +That is where this module comes in. When importing such a statement, this +module reconciles all the counterparts on the clearing account with one +another. Reconciliation is executed when validating the statement. When +reopening the statement, the reconcilation is undone. + +Known issues +============ +This module does not come with its own tests because it depends on a +statement filter being installed. Instead, it is tested in +`account_bank_statement_import_adyen` diff --git a/account_bank_statement_clearing_account/readme/HISTORY.rst b/account_bank_statement_clearing_account/readme/HISTORY.rst new file mode 100644 index 000000000..e69de29bb diff --git a/account_bank_statement_clearing_account/readme/INSTALL.rst b/account_bank_statement_clearing_account/readme/INSTALL.rst new file mode 100644 index 000000000..e69de29bb diff --git a/account_bank_statement_clearing_account/readme/ROADMAP.rst b/account_bank_statement_clearing_account/readme/ROADMAP.rst new file mode 100644 index 000000000..e69de29bb diff --git a/account_bank_statement_clearing_account/readme/USAGE.rst b/account_bank_statement_clearing_account/readme/USAGE.rst new file mode 100644 index 000000000..e69de29bb From b0ca78c7fb7de3869e3efed8524d2406f6384d1f Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 17 Feb 2021 16:26:15 +0100 Subject: [PATCH 05/24] [IMP] : black, isort, prettier --- .../__manifest__.py | 4 +- .../models/account_bank_statement.py | 24 ++-- .../__manifest__.py | 10 +- .../models/account_bank_statement_import.py | 97 +++++++------- .../models/account_journal.py | 14 +- .../tests/test_import_adyen.py | 121 ++++++++++-------- .../views/account_journal.xml | 6 +- .../account_bank_statement_clearing_account | 1 + .../setup.py | 6 + .../account_bank_statement_import_adyen | 1 + .../setup.py | 6 + 11 files changed, 161 insertions(+), 129 deletions(-) create mode 120000 setup/account_bank_statement_clearing_account/odoo/addons/account_bank_statement_clearing_account create mode 100644 setup/account_bank_statement_clearing_account/setup.py create mode 120000 setup/account_bank_statement_import_adyen/odoo/addons/account_bank_statement_import_adyen create mode 100644 setup/account_bank_statement_import_adyen/setup.py diff --git a/account_bank_statement_clearing_account/__manifest__.py b/account_bank_statement_clearing_account/__manifest__.py index 04d914fc7..5f6e364fa 100644 --- a/account_bank_statement_clearing_account/__manifest__.py +++ b/account_bank_statement_clearing_account/__manifest__.py @@ -8,8 +8,6 @@ "category": "Banking addons", "website": "https://opener.am", "license": "AGPL-3", - "depends": [ - "account_cancel", - ], + "depends": ["account_cancel",], "installable": True, } diff --git a/account_bank_statement_clearing_account/models/account_bank_statement.py b/account_bank_statement_clearing_account/models/account_bank_statement.py index eb30d3738..cf5ebadb4 100644 --- a/account_bank_statement_clearing_account/models/account_bank_statement.py +++ b/account_bank_statement_clearing_account/models/account_bank_statement.py @@ -5,7 +5,7 @@ class BankStatement(models.Model): - _inherit = 'account.bank.statement' + _inherit = "account.bank.statement" @api.multi def get_reconcile_clearing_account_lines(self): @@ -16,23 +16,25 @@ def get_reconcile_clearing_account_lines(self): the counterpart lines is zero. """ self.ensure_one() - if (self.journal_id.default_debit_account_id != - self.journal_id.default_credit_account_id or - not self.journal_id.default_debit_account_id.reconcile): + if ( + self.journal_id.default_debit_account_id + != self.journal_id.default_credit_account_id + or not self.journal_id.default_debit_account_id.reconcile + ): return False account = self.journal_id.default_debit_account_id currency = self.journal_id.currency_id or self.company_id.currency_id def get_bank_line(st_line): for line in st_line.journal_entry_ids: - field = 'debit' if st_line.amount > 0 else 'credit' - if (line.account_id == account and - not currency.compare_amounts( - line[field], abs(st_line.amount))): + field = "debit" if st_line.amount > 0 else "credit" + if line.account_id == account and not currency.compare_amounts( + line[field], abs(st_line.amount) + ): return line return False - move_lines = self.env['account.move.line'] + move_lines = self.env["account.move.line"] for st_line in self.line_ids: bank_line = get_bank_line(st_line) if not bank_line: @@ -50,8 +52,8 @@ def reconcile_clearing_account(self): self.ensure_one() lines = self.get_reconcile_clearing_account_lines() if not lines or any( - li.matched_debit_ids or li.matched_credit_ids - for li in lines): + li.matched_debit_ids or li.matched_credit_ids for li in lines + ): return False lines.reconcile() return True diff --git a/account_bank_statement_import_adyen/__manifest__.py b/account_bank_statement_import_adyen/__manifest__.py index 4be97c2ef..de2710321 100644 --- a/account_bank_statement_import_adyen/__manifest__.py +++ b/account_bank_statement_import_adyen/__manifest__.py @@ -12,13 +12,7 @@ "account_bank_statement_import", "account_bank_statement_clearing_account", ], - "external_dependencies": { - "python": [ - "openpyxl", - ], - }, - "data": [ - "views/account_journal.xml", - ], + "external_dependencies": {"python": ["openpyxl",],}, + "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 a8d17f60f..45e062de6 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,16 +1,17 @@ # © 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 odoo import api, models, fields +from openpyxl import load_workbook + +from odoo import api, fields, models from odoo.exceptions import UserError from odoo.tools.translate import _ class AccountBankStatementImport(models.TransientModel): - _inherit = 'account.bank.statement.import' + _inherit = "account.bank.statement.import" @api.model def _parse_file(self, data_file): @@ -19,41 +20,46 @@ def _parse_file(self, data_file): try: return self.import_adyen_xlsx(data_file) except ValueError: - return super(AccountBankStatementImport, self)._parse_file( - data_file) + return super(AccountBankStatementImport, self)._parse_file(data_file) 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', '=', account_number)], limit=1) + journal = self.env["account.journal"].search( + [("adyen_merchant_account", "=", account_number)], limit=1 + ) if journal: - if self._context.get('journal_id', journal.id) != journal.id: + 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) + _( + "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) + currency_code, account_number + ) @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)) + row[i] if row[i] else 0.0 for i in (16, 17, 18, 19, 20) + ) @api.model def import_adyen_transaction(self, statement, statement_id, row): - transaction_id = str(len(statement['transactions'])).zfill(4) + 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]), + note="{} {} {} {}".format(row[2], row[3], row[4], row[21]), name="%s" % (row[3] or row[4] or row[9]), ) - statement['transactions'].append(transaction) + statement["transactions"].append(transaction) @api.model def import_adyen_xlsx(self, data_file): @@ -75,61 +81,62 @@ def import_adyen_xlsx(self, data_file): 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)) + "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': + if row[1] != "Company Account": raise ValueError( 'Not an Adyen statement. Unexpected header "%s" ' - 'instead of "Company Account"', row[1]) + 'instead of "Company Account"', + row[1], + ) headers = True continue if not statement: - statement = {'transactions': []} + statement = {"transactions": []} statements.append(statement) - statement_id = '%s %s/%s' % ( - row[2], row[6].strftime('%Y'), int(row[23])) + statement_id = "{} {}/{}".format( + row[2], + row[6].strftime("%Y"), + int(row[23]), + ) currency_code = row[14] merchant_id = row[2] - statement['name'] = '%s %s/%s' % ( - row[2], row[6].year, row[23]) + statement["name"] = "{} {}/{}".format(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 + if not statement.get("date") or statement.get("date") > date: + statement["date"] = date row[8] = row[8].strip() - if row[8] == 'MerchantPayout': + if row[8] == "MerchantPayout": payout -= self.balance(row) else: balance += self.balance(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)) + 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.') + raise ValueError("Not an Adyen statement. Did not encounter header row.") if fees: - transaction_id = str(len(statement['transactions'])).zfill(4) + 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']), + date=max(t["date"] for t in statement["transactions"]), amount=-fees, - name='Commission, markup etc. batch %s' % (int(row[23])), + name="Commission, markup etc. batch %s" % (int(row[23])), ) balance -= fees - statement['transactions'].append(transaction) + statement["transactions"].append(transaction) - if statement['transactions'] and not payout: + 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( - _('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)) + _("Parse error. Balance %s not equal to merchant " "payout %s") + % (balance, payout) + ) 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 44a3e7f87..32c213dd6 100644 --- a/account_bank_statement_import_adyen/models/account_journal.py +++ b/account_bank_statement_import_adyen/models/account_journal.py @@ -5,14 +5,16 @@ class Journal(models.Model): - _inherit = 'account.journal' + _inherit = "account.journal" adyen_merchant_account = fields.Char( - help=('Fill in the exact merchant account string to select this ' - 'journal when importing Adyen statements')) + 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') + res = super(Journal, self)._get_bank_statements_available_import_formats() + res.append("adyen") return res 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 acbadfdf9..10e61e924 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -3,24 +3,27 @@ # © 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 +from odoo.tests.common import SavepointCase 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, - }) + 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 @@ -30,79 +33,91 @@ def test_01_import_adyen(self): 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) + "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))) + sum(line.amount for line in statement.line_ids) + ) + ) - account = self.env['account.account'].search([( - 'internal_type', '=', 'receivable')], limit=1) + account = self.env["account.account"].search( + [("internal_type", "=", "receivable")], limit=1 + ) for line in statement.line_ids: - 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}]) + 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, + } + ] + ) 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('full_reconcile_id') + 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("full_reconcile_id") self.assertEqual(len(reconcile), 1) 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, 'open') - self.assertFalse(lines.mapped('matched_debit_ids')) - self.assertFalse(lines.mapped('matched_credit_ids')) - self.assertFalse(lines.mapped('full_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")) # 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')) + 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') + "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'): + with self.assertRaisesRegex(UserError, "Could not make sense"): self._test_statement_import( - 'account_bank_statement_import_adyen', - 'adyen_test_invalid.xls', - 'invalid') + "account_bank_statement_import_adyen", + "adyen_test_invalid.xls", + "invalid", + ) - def _test_statement_import( - self, module_name, file_name, statement_name): + 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_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} ) - 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)]) + 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 02f36f051..bf845158c 100644 --- a/account_bank_statement_import_adyen/views/account_journal.xml +++ b/account_bank_statement_import_adyen/views/account_journal.xml @@ -1,12 +1,12 @@ - + Add Adyen merchant account account.journal - + - + diff --git a/setup/account_bank_statement_clearing_account/odoo/addons/account_bank_statement_clearing_account b/setup/account_bank_statement_clearing_account/odoo/addons/account_bank_statement_clearing_account new file mode 120000 index 000000000..4ae7bb48d --- /dev/null +++ b/setup/account_bank_statement_clearing_account/odoo/addons/account_bank_statement_clearing_account @@ -0,0 +1 @@ +../../../../account_bank_statement_clearing_account \ No newline at end of file diff --git a/setup/account_bank_statement_clearing_account/setup.py b/setup/account_bank_statement_clearing_account/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/account_bank_statement_clearing_account/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/account_bank_statement_import_adyen/odoo/addons/account_bank_statement_import_adyen b/setup/account_bank_statement_import_adyen/odoo/addons/account_bank_statement_import_adyen new file mode 120000 index 000000000..2e39464ec --- /dev/null +++ b/setup/account_bank_statement_import_adyen/odoo/addons/account_bank_statement_import_adyen @@ -0,0 +1 @@ +../../../../account_bank_statement_import_adyen \ No newline at end of file diff --git a/setup/account_bank_statement_import_adyen/setup.py b/setup/account_bank_statement_import_adyen/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/account_bank_statement_import_adyen/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 172c4be6477143126a1fd1c10af7a58d4ab82729 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 17 Feb 2021 16:35:49 +0100 Subject: [PATCH 06/24] [MIG] account_bank_statement_clearing_account Migrate to 13.0 --- .../README.rst | 123 ++++- .../__manifest__.py | 12 +- .../models/account_bank_statement.py | 17 +- .../readme/CONTRIBUTORS.rst | 1 + .../readme/USAGE.rst | 3 + .../static/description/index.html | 442 ++++++++++++++++++ 6 files changed, 557 insertions(+), 41 deletions(-) create mode 100644 account_bank_statement_clearing_account/static/description/index.html diff --git a/account_bank_statement_clearing_account/README.rst b/account_bank_statement_clearing_account/README.rst index 38929e877..830495f80 100644 --- a/account_bank_statement_clearing_account/README.rst +++ b/account_bank_statement_clearing_account/README.rst @@ -1,35 +1,110 @@ -**This file is going to be generated by oca-gen-addon-readme.** +============================================= +Reconcile entries from pseudo bank statements +============================================= -*Manual changes will be overwritten.* +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -Please provide content in the ``readme`` directory: +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fbank--statement--import-lightgray.png?logo=github + :target: https://github.com/OCA/bank-statement-import/tree/13.0/account_bank_statement_clearing_account + :alt: OCA/bank-statement-import +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/bank-statement-import-13-0/bank-statement-import-13-0-account_bank_statement_clearing_account + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/174/13.0 + :alt: Try me on Runbot -* **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) +|badge1| |badge2| |badge3| |badge4| |badge5| -Content of this README will also be drawn from the addon manifest, -from keys such as name, authors, maintainers, development_status, -and license. +This is a technical modules that you can use to improve the processing of +statement files from payment providers. These statements usually consist +of lines that to be reconciled by customer debts, offset by lines that are +to be reconciled by the imbursements from the payment provider, corrected +for customer credits and the costs of the payment provider. Typically, the +balance of such a statement is zero. Effectively, the counterpart of each +statement line is made on a clearing account and you should keep track of +the balance of the clearing account to see if the payment provider still owes +you money. You can keep track of the account by reconciling each entry on it. -A good, one sentence summary in the manifest is also highly recommended. +That is where this module comes in. When importing such a statement, this +module reconciles all the counterparts on the clearing account with one +another. Reconciliation is executed when validating the statement. When +reopening the statement, the reconcilation is undone. +Known issues +============ +This module does not come with its own tests because it depends on a +statement filter being installed. Instead, it is tested in +`account_bank_statement_import_adyen` -Automatic changelog generation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +**Table of contents** -`HISTORY.rst` can be auto generated using `towncrier `_. +.. contents:: + :local: -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. +Configuration +============= -Please refer to `towncrier` documentation to know more. +In order to enable the reconcilation of the counterparts of zero-balance +statement files from payment providers, you need to make sure that the journal +that is used for these statements have the same default debit account as their +default credit account, and this account is configured for reconciliation. -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 `_. +Usage +===== + +After installing this module, any statement where the sum of all debit and +credit lines match, and where the default journal account is reconcilable, will +reconcile all posted moved when the statement is confirmed. + +Bug Tracker +=========== + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Opener B.V. +* Vanmoof BV + +Contributors +~~~~~~~~~~~~ + +* Stefan Rijnhart (https://opener.amsterdam) +* Martin Pishpecki (https://www.vanmoof.com) +* Ronald Portier (https://therp.nl) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/bank-statement-import `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_bank_statement_clearing_account/__manifest__.py b/account_bank_statement_clearing_account/__manifest__.py index 5f6e364fa..91148a50b 100644 --- a/account_bank_statement_clearing_account/__manifest__.py +++ b/account_bank_statement_clearing_account/__manifest__.py @@ -1,13 +1,13 @@ -# © 2017 Opener BV () -# © 2020 Vanmoof BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2017 Opener BV () +# Copyright 2020 Vanmoof BV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "Reconcile entries from pseudo bank statements", - "version": "12.0.1.0.0", + "version": "13.0.1.0.0", "author": "Opener B.V., Vanmoof BV, Odoo Community Association (OCA)", "category": "Banking addons", - "website": "https://opener.am", + "website": "https://github.com/oca/bank-statement-import", "license": "AGPL-3", - "depends": ["account_cancel",], + "depends": ["account"], "installable": True, } diff --git a/account_bank_statement_clearing_account/models/account_bank_statement.py b/account_bank_statement_clearing_account/models/account_bank_statement.py index cf5ebadb4..79baf5dcf 100644 --- a/account_bank_statement_clearing_account/models/account_bank_statement.py +++ b/account_bank_statement_clearing_account/models/account_bank_statement.py @@ -1,13 +1,12 @@ -# © 2017 Opener BV () -# © 2020 Vanmoof BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, models +# Copyright 2017 Opener BV () +# Copyright 2020 Vanmoof BV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import models class BankStatement(models.Model): _inherit = "account.bank.statement" - @api.multi def get_reconcile_clearing_account_lines(self): """ If this statement qualifies for clearing account reconciliation, return the relevant lines to (un)reconcile. This is the case if the @@ -45,7 +44,6 @@ def get_bank_line(st_line): return False return move_lines - @api.multi def reconcile_clearing_account(self): """ If applicable, reconcile the clearing account lines in case all lines are still unreconciled. """ @@ -58,7 +56,6 @@ def reconcile_clearing_account(self): lines.reconcile() return True - @api.multi def unreconcile_clearing_account(self): """ If applicable, unreconcile the clearing account lines if still fully reconciled with each other. """ @@ -72,16 +69,14 @@ def unreconcile_clearing_account(self): return True return False - @api.multi - def button_draft(self): + def button_reopen(self): """ When setting the statement back to draft, unreconcile the reconciliation on the clearing account """ - res = super(BankStatement, self).button_draft() + res = super(BankStatement, self).button_reopen() for statement in self: statement.unreconcile_clearing_account() return res - @api.multi def button_confirm_bank(self): """ When confirming the statement, trigger the reconciliation of the lines on the clearing account (if applicable) """ diff --git a/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst b/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst index 58e9f494c..f00439e56 100644 --- a/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst +++ b/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst @@ -1,2 +1,3 @@ * Stefan Rijnhart (https://opener.amsterdam) * Martin Pishpecki (https://www.vanmoof.com) +* Ronald Portier (https://therp.nl) diff --git a/account_bank_statement_clearing_account/readme/USAGE.rst b/account_bank_statement_clearing_account/readme/USAGE.rst index e69de29bb..9f9ef0acc 100644 --- a/account_bank_statement_clearing_account/readme/USAGE.rst +++ b/account_bank_statement_clearing_account/readme/USAGE.rst @@ -0,0 +1,3 @@ +After installing this module, any statement where the sum of all debit and +credit lines match, and where the default journal account is reconcilable, will +reconcile all posted moved when the statement is confirmed. diff --git a/account_bank_statement_clearing_account/static/description/index.html b/account_bank_statement_clearing_account/static/description/index.html new file mode 100644 index 000000000..ba3839a4d --- /dev/null +++ b/account_bank_statement_clearing_account/static/description/index.html @@ -0,0 +1,442 @@ + + + + + + +Reconcile entries from pseudo bank statements + + + +
+

Reconcile entries from pseudo bank statements

+ + +

Beta License: AGPL-3 OCA/bank-statement-import Translate me on Weblate Try me on Runbot

+

This is a technical modules that you can use to improve the processing of +statement files from payment providers. These statements usually consist +of lines that to be reconciled by customer debts, offset by lines that are +to be reconciled by the imbursements from the payment provider, corrected +for customer credits and the costs of the payment provider. Typically, the +balance of such a statement is zero. Effectively, the counterpart of each +statement line is made on a clearing account and you should keep track of +the balance of the clearing account to see if the payment provider still owes +you money. You can keep track of the account by reconciling each entry on it.

+

That is where this module comes in. When importing such a statement, this +module reconciles all the counterparts on the clearing account with one +another. Reconciliation is executed when validating the statement. When +reopening the statement, the reconcilation is undone.

+
+

Known issues

+

This module does not come with its own tests because it depends on a +statement filter being installed. Instead, it is tested in +account_bank_statement_import_adyen

+

Table of contents

+
+
+

Configuration

+

In order to enable the reconcilation of the counterparts of zero-balance +statement files from payment providers, you need to make sure that the journal +that is used for these statements have the same default debit account as their +default credit account, and this account is configured for reconciliation.

+
+
+

Usage

+

After installing this module, any statement where the sum of all debit and +credit lines match, and where the default journal account is reconcilable, will +reconcile all posted moved when the statement is confirmed.

+
+
+

Bug Tracker

+

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 smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Opener B.V.
  • +
  • Vanmoof BV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/bank-statement-import project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + From e5a99def30de97d32cd3313d1979a558bdfff5fc Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 17 Feb 2021 16:55:26 +0100 Subject: [PATCH 07/24] [MIG] account_bank_statement_import_adyen Migrate to 13.0 --- .../readme/CREDITS.rst | 0 .../readme/HISTORY.rst | 0 .../readme/INSTALL.rst | 0 .../readme/ROADMAP.rst | 0 .../tests/__init__.py | 1 + .../tests/test_clearing_account.py | 120 +++++ .../README.rst | 110 ++++- .../__manifest__.py | 13 +- .../models/account_bank_statement_import.py | 115 +++-- .../models/account_journal.py | 10 +- .../readme/CONFIGURE.rst | 6 +- .../readme/CONTRIBUTORS.rst | 1 + .../readme/CREDITS.rst | 0 .../readme/DESCRIPTION.rst | 8 +- .../readme/HISTORY.rst | 0 .../readme/INSTALL.rst | 0 .../readme/ROADMAP.rst | 0 .../static/description/index.html | 442 ++++++++++++++++++ .../tests/test_import_adyen.py | 92 +--- 19 files changed, 745 insertions(+), 173 deletions(-) delete mode 100644 account_bank_statement_clearing_account/readme/CREDITS.rst delete mode 100644 account_bank_statement_clearing_account/readme/HISTORY.rst delete mode 100644 account_bank_statement_clearing_account/readme/INSTALL.rst delete mode 100644 account_bank_statement_clearing_account/readme/ROADMAP.rst create mode 100644 account_bank_statement_clearing_account/tests/__init__.py create mode 100644 account_bank_statement_clearing_account/tests/test_clearing_account.py delete mode 100644 account_bank_statement_import_adyen/readme/CREDITS.rst delete mode 100644 account_bank_statement_import_adyen/readme/HISTORY.rst delete mode 100644 account_bank_statement_import_adyen/readme/INSTALL.rst delete mode 100644 account_bank_statement_import_adyen/readme/ROADMAP.rst create mode 100644 account_bank_statement_import_adyen/static/description/index.html diff --git a/account_bank_statement_clearing_account/readme/CREDITS.rst b/account_bank_statement_clearing_account/readme/CREDITS.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/account_bank_statement_clearing_account/readme/HISTORY.rst b/account_bank_statement_clearing_account/readme/HISTORY.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/account_bank_statement_clearing_account/readme/INSTALL.rst b/account_bank_statement_clearing_account/readme/INSTALL.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/account_bank_statement_clearing_account/readme/ROADMAP.rst b/account_bank_statement_clearing_account/readme/ROADMAP.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/account_bank_statement_clearing_account/tests/__init__.py b/account_bank_statement_clearing_account/tests/__init__.py new file mode 100644 index 000000000..3e17ae026 --- /dev/null +++ b/account_bank_statement_clearing_account/tests/__init__.py @@ -0,0 +1 @@ +from . import test_clearing_account diff --git a/account_bank_statement_clearing_account/tests/test_clearing_account.py b/account_bank_statement_clearing_account/tests/test_clearing_account.py new file mode 100644 index 000000000..e698da0a6 --- /dev/null +++ b/account_bank_statement_clearing_account/tests/test_clearing_account.py @@ -0,0 +1,120 @@ +# Copyright 2017 Opener BV +# Copyright 2020 Vanmoof BV +# Copyright 2015-2021 Therp BV ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo.tests.common import SavepointCase + + +class TestClearingAccount(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.journal = cls.env["account.journal"].create( + { + "company_id": cls.env.user.company_id.id, + "name": "Clearing account test", + "code": "CAT", + "type": "bank", + "currency_id": cls.env.ref("base.USD").id, + } + ) + cls.partner_customer = cls.env["res.partner"].create({"name": "cutomer"}) + cls.partner_provider = cls.env["res.partner"].create({"name": "provider"}) + # 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_reconcile_unreconcile(self): + """Test that a bank statement that satiesfies the conditions, cab be + automatically reconciled and unreconciled on confirmation or reset of the + statement. + """ + account = self.journal.default_debit_account_id + statement = self.env["account.bank.statement"].create( + { + "name": "Test autoreconcile 2021-03-08", + "reference": "AUTO-2021-08-03", + "date": "2021-03-08", + "state": "open", + "journal_id": self.journal.id, + "line_ids": [ + ( + 0, + 0, + { + "name": "web sale", + "partner_id": self.partner_customer.id, + "amount": 100.00, + "account_id": account.id, + }, + ), + ( + 0, + 0, + { + "name": "transaction_fees", + "partner_id": False, + "amount": -1.25, + "account_id": account.id, + }, + ), + ( + 0, + 0, + { + "name": "due_from_provider", + "partner_id": self.partner_provider.id, + "amount": -98.75, + "account_id": account.id, + }, + ), + ], + } + ) + self.assertEqual(statement.journal_id, self.journal) + self.assertEqual(len(statement.line_ids), 3) + 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( + [("internal_type", "=", "receivable")], limit=1 + ) + for line in statement.line_ids: + 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, + } + ] + ) + 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("full_reconcile_id") + self.assertEqual(len(reconcile), 1) + self.assertEqual(lines, reconcile.reconciled_line_ids) + + # Reset the bank statement to see the counterpart lines being + # unreconciled + statement.button_reopen() + 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")) + + # 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")) diff --git a/account_bank_statement_import_adyen/README.rst b/account_bank_statement_import_adyen/README.rst index 38929e877..5a8d54c16 100644 --- a/account_bank_statement_import_adyen/README.rst +++ b/account_bank_statement_import_adyen/README.rst @@ -1,35 +1,97 @@ -**This file is going to be generated by oca-gen-addon-readme.** +====================== +Adyen statement import +====================== -*Manual changes will be overwritten.* +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -Please provide content in the ``readme`` directory: +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fbank--statement--import-lightgray.png?logo=github + :target: https://github.com/OCA/bank-statement-import/tree/13.0/account_bank_statement_import_adyen + :alt: OCA/bank-statement-import +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/bank-statement-import-13-0/bank-statement-import-13-0-account_bank_statement_import_adyen + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/174/13.0 + :alt: Try me on Runbot -* **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) +|badge1| |badge2| |badge3| |badge4| |badge5| -Content of this README will also be drawn from the addon manifest, -from keys such as name, authors, maintainers, development_status, -and license. +This module processes Adyen transaction statements, the settlement details report, +in excel or csv format. -A good, one sentence summary in the manifest is also highly recommended. +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. +**Table of contents** -Automatic changelog generation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. contents:: + :local: -`HISTORY.rst` can be auto generated using `towncrier `_. +Configuration +============= -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. +Configure a pseudo bank journal by creating a new journal linked to your Adyen +merchant account. Set your merchant account string in the Advanced settings +on the journal form. -Please refer to `towncrier` documentation to know more. +Usage +===== -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 `_. +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. + +Bug Tracker +=========== + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Opener BV +* Vanmoof BV + +Contributors +~~~~~~~~~~~~ + +* Stefan Rijnhart (https://opener.amsterdam) +* Martin Pishpecki (https://www.vanmoof.com) +* Ronald Portier (https://therp.nl) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/bank-statement-import `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_bank_statement_import_adyen/__manifest__.py b/account_bank_statement_import_adyen/__manifest__.py index de2710321..bc24a336f 100644 --- a/account_bank_statement_import_adyen/__manifest__.py +++ b/account_bank_statement_import_adyen/__manifest__.py @@ -1,18 +1,19 @@ -# © 2017 Opener BV () -# © 2020 Vanmoof BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2017 Opener BV +# Copyright 2020 Vanmoof BV +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "Adyen statement import", - "version": "12.0.1.0.0", + "version": "13.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": [ + "base_import", "account_bank_statement_import", "account_bank_statement_clearing_account", ], - "external_dependencies": {"python": ["openpyxl",],}, - "data": ["views/account_journal.xml",], + "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 45e062de6..0ba9b7ba8 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,13 +1,12 @@ -# © 2017 Opener BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from io import BytesIO -from zipfile import BadZipfile +# Copyright 2017 Opener BV () +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import logging -from openpyxl import load_workbook - -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.exceptions import UserError -from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) class AccountBankStatementImport(models.TransientModel): @@ -15,15 +14,14 @@ class AccountBankStatementImport(models.TransientModel): @api.model def _parse_file(self, data_file): - """Parse an Adyen xlsx file and map merchant account strings - to journals. """ + """Parse an Adyen xlsx file and map merchant account strings to journals.""" try: return self.import_adyen_xlsx(data_file) except ValueError: - return super(AccountBankStatementImport, self)._parse_file(data_file) + return super()._parse_file(data_file) def _find_additional_data(self, currency_code, account_number): - """ Try to find journal by Adyen merchant account """ + """Try to find journal by Adyen merchant account.""" if account_number: journal = self.env["account.journal"].search( [("adyen_merchant_account", "=", account_number)], limit=1 @@ -39,14 +37,12 @@ def _find_additional_data(self, currency_code, account_number): % account_number ) self = self.with_context(journal_id=journal.id) - return super(AccountBankStatementImport, self)._find_additional_data( - currency_code, account_number - ) + return super()._find_additional_data(currency_code, account_number) @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) + return -(float(row[15]) if row[15] else 0.0) + sum( + float(row[i]) if row[i] else 0.0 for i in (16, 17, 18, 19, 20) ) @api.model @@ -62,7 +58,7 @@ def import_adyen_transaction(self, statement, statement_id, row): statement["transactions"].append(transaction) @api.model - def import_adyen_xlsx(self, data_file): + def parse_adyen_file(self, data_file): statements = [] statement = None headers = False @@ -71,52 +67,49 @@ def import_adyen_xlsx(self, data_file): payout = 0.0 statement_id = None - 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: + import_model = self.env["base_import.import"] + importer = import_model.create( + {"file": data_file, "file_name": "Ayden settlemnt details"} + ) + rows = importer._read_file({}) + + for row in rows: + 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 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 = {"transactions": []} - statements.append(statement) - statement_id = "{} {}/{}".format( - row[2], - row[6].strftime("%Y"), - int(row[23]), + 'Not an Adyen statement. Unexpected header "%s" ' + 'instead of "Company Account"', + row[1], ) - currency_code = row[14] - merchant_id = row[2] - statement["name"] = "{} {}/{}".format(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 + headers = True + continue + if not statement: + statement = {"transactions": []} + statements.append(statement) + statement_id = "{merchant} {year}/{batch}".format( + merchant=row[2], year=row[6][:4], batch=row[23], + ) + currency_code = row[14] + merchant_id = row[2] + statement["name"] = statement_id + 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, statement_id, row) - fees += sum(row[i] if row[i] else 0.0 for i in (17, 18, 19, 20)) + row[8] = row[8].strip() + if row[8] == "MerchantPayout": + payout -= self.balance(row) + else: + balance += self.balance(row) + self.import_adyen_transaction(statement, statement_id, row) + fees += sum(float(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.") diff --git a/account_bank_statement_import_adyen/models/account_journal.py b/account_bank_statement_import_adyen/models/account_journal.py index 32c213dd6..605fc5282 100644 --- a/account_bank_statement_import_adyen/models/account_journal.py +++ b/account_bank_statement_import_adyen/models/account_journal.py @@ -1,10 +1,10 @@ -# © 2017 Opener BV () -# © 2020 Vanmoof BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2017 Opener BV () +# Copyright 2020 Vanmoof BV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import fields, models -class Journal(models.Model): +class AccountJournal(models.Model): _inherit = "account.journal" adyen_merchant_account = fields.Char( @@ -15,6 +15,6 @@ class Journal(models.Model): ) def _get_bank_statements_available_import_formats(self): - res = super(Journal, self)._get_bank_statements_available_import_formats() + res = super()._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 index 7df8a95ee..0f7870bd0 100644 --- a/account_bank_statement_import_adyen/readme/CONFIGURE.rst +++ b/account_bank_statement_import_adyen/readme/CONFIGURE.rst @@ -1,3 +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. +Configure a pseudo bank journal by creating a new journal linked to your Adyen +merchant 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 index 58e9f494c..f00439e56 100644 --- a/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst +++ b/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst @@ -1,2 +1,3 @@ * Stefan Rijnhart (https://opener.amsterdam) * Martin Pishpecki (https://www.vanmoof.com) +* Ronald Portier (https://therp.nl) diff --git a/account_bank_statement_import_adyen/readme/CREDITS.rst b/account_bank_statement_import_adyen/readme/CREDITS.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/account_bank_statement_import_adyen/readme/DESCRIPTION.rst b/account_bank_statement_import_adyen/readme/DESCRIPTION.rst index c0832e16e..c07218ef5 100644 --- a/account_bank_statement_import_adyen/readme/DESCRIPTION.rst +++ b/account_bank_statement_import_adyen/readme/DESCRIPTION.rst @@ -1,9 +1,7 @@ -====================== -Adyen statement import -====================== +This module processes Adyen transaction statements, the settlement details report, +in excel or csv format. -This module processes Adyen transaction statements in xlsx format. You can -import the statements in a dedicated journal. Reconcile your sale invoices +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 diff --git a/account_bank_statement_import_adyen/readme/HISTORY.rst b/account_bank_statement_import_adyen/readme/HISTORY.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/account_bank_statement_import_adyen/readme/INSTALL.rst b/account_bank_statement_import_adyen/readme/INSTALL.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/account_bank_statement_import_adyen/readme/ROADMAP.rst b/account_bank_statement_import_adyen/readme/ROADMAP.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/account_bank_statement_import_adyen/static/description/index.html b/account_bank_statement_import_adyen/static/description/index.html new file mode 100644 index 000000000..7d826df91 --- /dev/null +++ b/account_bank_statement_import_adyen/static/description/index.html @@ -0,0 +1,442 @@ + + + + + + +Adyen statement import + + + +
+

Adyen statement import

+ + +

Beta License: AGPL-3 OCA/bank-statement-import Translate me on Weblate Try me on Runbot

+

This module processes Adyen transaction statements, the settlement details report, +in excel or csv 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.

+

Table of contents

+ +
+

Configuration

+

Configure a pseudo bank journal by creating a new journal linked to your Adyen +merchant 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.

+
+
+

Bug Tracker

+

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 smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Opener BV
  • +
  • Vanmoof BV
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/bank-statement-import project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + 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 10e61e924..1fc5418f7 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -1,7 +1,7 @@ -# © 2017 Opener BV () -# © 2020 Vanmoof BV () -# © 2015 Therp BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2017 Opener BV +# Copyright 2020 Vanmoof BV +# Copyright 2015-2021 Therp BV ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import base64 from odoo.exceptions import UserError @@ -12,7 +12,7 @@ class TestImportAdyen(SavepointCase): @classmethod def setUpClass(cls): - super(TestImportAdyen, cls).setUpClass() + super().setUpClass() cls.journal = cls.env["account.journal"].create( { "company_id": cls.env.user.company_id.id, @@ -20,7 +20,6 @@ def setUpClass(cls): "code": "ADY", "type": "bank", "adyen_merchant_account": "YOURCOMPANY_ACCOUNT", - "update_posted": True, "currency_id": cls.env.ref("base.USD").id, } ) @@ -33,9 +32,7 @@ def test_01_import_adyen(self): 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", + "adyen_test.xlsx", "YOURCOMPANY_ACCOUNT 2016/48", ) statement = self.env["account.bank.statement"].search( [], order="create_date desc", limit=1 @@ -48,76 +45,33 @@ def test_01_import_adyen(self): ) ) - account = self.env["account.account"].search( - [("internal_type", "=", "receivable")], limit=1 - ) - for line in statement.line_ids: - 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, - } - ] - ) - - 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("full_reconcile_id") - self.assertEqual(len(reconcile), 1) - 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, "open") - self.assertFalse(lines.mapped("matched_debit_ids")) - self.assertFalse(lines.mapped("matched_credit_ids")) - self.assertFalse(lines.mapped("full_reconcile_id")) - - # 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", + "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", + "adyen_test_invalid.xls", "invalid", ) - def _test_statement_import(self, module_name, file_name, statement_name): + def _test_statement_import(self, 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)] + testfile = get_module_resource( + "account_bank_statement_import_adyen", "test_files", file_name ) - self.assertTrue(statements) - return statements + with open(testfile, "rb") as datafile: + data_file = base64.b64encode(datafile.read()) + import_wizard = self.env["account.bank.statement.import"].create( + {"attachment_ids": [(0, 0, {"name": "test file", "datas": data_file})]} + ) + 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 From 154a025410c17eb206f2e8ec42434ef1e6b0c979 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 17 Feb 2021 18:08:42 +0100 Subject: [PATCH 08/24] [ADD] account_bank_statement_import_online_adyen --- .../models/account_bank_statement_import.py | 29 +- .../tests/test_import_adyen.py | 7 +- .../README.rst | 118 +++++ .../__init__.py | 3 + .../__manifest__.py | 16 + .../models/__init__.py | 4 + .../models/account_journal.py | 30 ++ .../models/online_bank_statement_provider.py | 100 ++++ .../readme/CONFIGURE.rst | 27 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 1 + .../readme/USAGE.rst | 10 + .../static/description/icon.png | Bin 0 -> 11070 bytes .../static/description/index.html | 464 ++++++++++++++++++ .../tests/__init__.py | 2 + .../online_bank_statement_provider_dummy.py | 23 + .../tests/test_import_online.py | 66 +++ .../views/online_bank_statement_provider.xml | 28 ++ 18 files changed, 917 insertions(+), 12 deletions(-) create mode 100644 account_bank_statement_import_online_adyen/README.rst create mode 100644 account_bank_statement_import_online_adyen/__init__.py create mode 100644 account_bank_statement_import_online_adyen/__manifest__.py create mode 100644 account_bank_statement_import_online_adyen/models/__init__.py create mode 100644 account_bank_statement_import_online_adyen/models/account_journal.py create mode 100644 account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py create mode 100644 account_bank_statement_import_online_adyen/readme/CONFIGURE.rst create mode 100644 account_bank_statement_import_online_adyen/readme/CONTRIBUTORS.rst create mode 100644 account_bank_statement_import_online_adyen/readme/DESCRIPTION.rst create mode 100644 account_bank_statement_import_online_adyen/readme/USAGE.rst create mode 100644 account_bank_statement_import_online_adyen/static/description/icon.png create mode 100644 account_bank_statement_import_online_adyen/static/description/index.html create mode 100644 account_bank_statement_import_online_adyen/tests/__init__.py create mode 100644 account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py create mode 100644 account_bank_statement_import_online_adyen/tests/test_import_online.py create mode 100644 account_bank_statement_import_online_adyen/views/online_bank_statement_provider.xml 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 0ba9b7ba8..4df68c578 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 @@ -16,8 +16,18 @@ class AccountBankStatementImport(models.TransientModel): def _parse_file(self, data_file): """Parse an Adyen xlsx file and map merchant account strings to journals.""" try: - return self.import_adyen_xlsx(data_file) + try: + return self._parse_adyen_file(data_file) + except Exception as exc: + if self.env.context.get("account_bank_statement_import_adyen", False): + raise + _logger.info("Adyen parser error", exc_info=True) + raise ValueError("Not an adyen settlements file: %s" % exc) except ValueError: + _logger.debug( + _("Statement file was not a Adyen settlement details file."), + exc_info=True, + ) return super()._parse_file(data_file) def _find_additional_data(self, currency_code, account_number): @@ -40,25 +50,25 @@ def _find_additional_data(self, currency_code, account_number): return super()._find_additional_data(currency_code, account_number) @api.model - def balance(self, row): + def _balance(self, row): return -(float(row[15]) if row[15] else 0.0) + sum( float(row[i]) if row[i] else 0.0 for i in (16, 17, 18, 19, 20) ) @api.model - def import_adyen_transaction(self, statement, statement_id, row): + 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), + amount=self._balance(row), note="{} {} {} {}".format(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 parse_adyen_file(self, data_file): + def _parse_adyen_file(self, data_file): statements = [] statement = None headers = False @@ -70,6 +80,9 @@ def parse_adyen_file(self, data_file): import_model = self.env["base_import.import"] importer = import_model.create( {"file": data_file, "file_name": "Ayden settlemnt details"} + import_model = self.env["base_import.import"] + importer = import_model.create( + {"file": data_file, "file_name": "Ayden settlement details"} ) rows = importer._read_file({}) @@ -105,10 +118,10 @@ def parse_adyen_file(self, data_file): row[8] = row[8].strip() if row[8] == "MerchantPayout": - payout -= self.balance(row) + payout -= self._balance(row) else: - balance += self.balance(row) - self.import_adyen_transaction(statement, statement_id, row) + balance += self._balance(row) + self._import_adyen_transaction(statement, statement_id, row) fees += sum(float(row[i]) if row[i] else 0.0 for i in (17, 18, 19, 20)) if not headers: 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 1fc5418f7..ca75591a9 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -23,9 +23,6 @@ def setUpClass(cls): "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_01_import_adyen(self): """ Test that the Adyen statement can be imported and that the @@ -68,7 +65,9 @@ def _test_statement_import(self, file_name, statement_name): import_wizard = self.env["account.bank.statement.import"].create( {"attachment_ids": [(0, 0, {"name": "test file", "datas": data_file})]} ) - import_wizard.import_file() + import_wizard.with_context( + {"account_bank_statement_import_adyen": True} + ).import_file() # statement name is account number + '-' + date of last line: statements = self.env["account.bank.statement"].search( [("name", "=", statement_name)] diff --git a/account_bank_statement_import_online_adyen/README.rst b/account_bank_statement_import_online_adyen/README.rst new file mode 100644 index 000000000..32b36ef10 --- /dev/null +++ b/account_bank_statement_import_online_adyen/README.rst @@ -0,0 +1,118 @@ +============================================ +Online Bank Statements: Adyen payment report +============================================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fbank--statement--import-lightgray.png?logo=github + :target: https://github.com/OCA/bank-statement-import/tree/13.0/account_bank_statement_import_online_adyen + :alt: OCA/bank-statement-import +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/bank-statement-import-13-0/bank-statement-import-13-0-account_bank_statement_import_online_adyen + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/174/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module automates the download and import of Adyen payment reports. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure online bank statements provider: + +#. Go to *Invoicing > Configuration > Bank Accounts* +#. Open bank account to configure and edit it +#. Set *Bank Feeds* to *Online* +#. Select *Adyen* as online bank statements provider in + *Online Bank Statements (OCA)* section +#. Save the bank account +#. Click on provider and configure provider-specific settings. + +or, alternatively: + +#. Go to *Invoicing > Overview* +#. Open settings of the corresponding journal account +#. Switch to *Bank Account* tab +#. Set *Bank Feeds* to *Online* +#. Select *Adyen* as online bank statements provider in + *Online Bank Statements (OCA)* section +#. Save the bank account +#. Click on provider and configure provider-specific settings. + +To obtain *Login* and *Key*: + +#. Open `Adyen website `_. + +Check also ``account_bank_statement_import_online`` configuration instructions +for more information. + +Usage +===== + +To pull historical bank statements: + +#. Go to *Invoicing > Configuration > Bank Accounts* +#. Select specific bank accounts +#. Launch *Actions > Online Bank Statements Pull Wizard* +#. Configure date interval and click *Pull* + +If historical data is not needed, then just simply wait for the scheduled +activity "Pull Online Bank Statements" to be executed for getting new +transactions. + +Bug Tracker +=========== + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Ronald Portier (Therp BV) + +Contributors +~~~~~~~~~~~~ + +* Ronald Portier - Therp BV + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/bank-statement-import `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_bank_statement_import_online_adyen/__init__.py b/account_bank_statement_import_online_adyen/__init__.py new file mode 100644 index 000000000..ad8c86482 --- /dev/null +++ b/account_bank_statement_import_online_adyen/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import models +from .tests import online_bank_statement_provider_dummy diff --git a/account_bank_statement_import_online_adyen/__manifest__.py b/account_bank_statement_import_online_adyen/__manifest__.py new file mode 100644 index 000000000..52ae345b6 --- /dev/null +++ b/account_bank_statement_import_online_adyen/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2021 - Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Online Bank Statements: Adyen payment report", + "version": "13.0.1.0.0", + "category": "Account", + "website": "https://github.com/OCA/bank-statement-import", + "author": "Ronald Portier (Therp BV), Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "depends": [ + "account_bank_statement_import_adyen", + "account_bank_statement_import_online", + ], + "data": ["views/online_bank_statement_provider.xml"], +} diff --git a/account_bank_statement_import_online_adyen/models/__init__.py b/account_bank_statement_import_online_adyen/models/__init__.py new file mode 100644 index 000000000..56bd827cc --- /dev/null +++ b/account_bank_statement_import_online_adyen/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import account_journal +from . import online_bank_statement_provider diff --git a/account_bank_statement_import_online_adyen/models/account_journal.py b/account_bank_statement_import_online_adyen/models/account_journal.py new file mode 100644 index 000000000..fb7572a39 --- /dev/null +++ b/account_bank_statement_import_online_adyen/models/account_journal.py @@ -0,0 +1,30 @@ +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, models + + +class AccountJournal(models.Model): + _inherit = "account.journal" + + def write(self, vals): + """Do not reset a provider to file_import, if that will delete provider.""" + # TODO: In the future place this in super account_bank_statement_import_online. + for this in self: + is_online = this.bank_statements_source == "online" + if is_online and vals.get("bank_statements_source", "online") != "online": + vals.pop("bank_statements_source") + super(AccountJournal, this).write(vals) + return True + + @api.model + def _selection_online_bank_statement_provider(self): + res = super()._selection_online_bank_statement_provider() + res.append(("dummy_adyen", "Dummy Adyen")) + return res + + @api.model + def values_online_bank_statement_provider(self): + res = super().values_online_bank_statement_provider() + if self.user_has_groups("base.group_no_one"): + res += [("dummy_adyen", "Dummy Adyen")] + return res diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py new file mode 100644 index 000000000..62a9babd2 --- /dev/null +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -0,0 +1,100 @@ +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging +from html import escape + +import requests + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class OnlineBankStatementProvider(models.Model): + _inherit = "online.bank.statement.provider" + + download_file_name = fields.Char() + next_batch_number = fields.Integer() + + @api.model + def _selection_service(self): + res = super()._selection_service() + res.append(("dummy_adyen", "Dummy Adyen")) + return res + + @api.model + def _get_available_services(self): + return super()._get_available_services() + [("adyen", "Adyen payment report")] + + def _pull(self, date_since, date_until): # noqa: C901 + """Split between adyen providers and others.""" + adyen_providers = self.filtered(lambda r: r.service in ("adyen", "dummy_adyen")) + other_providers = self.filtered( + lambda r: r.service not in ("adyen", "dummy_adyen") + ) + if other_providers: + super(OnlineBankStatementProvider, other_providers)._pull( + date_since, date_until + ) + for provider in adyen_providers: + # TODO: incrementing batch number + is_scheduled = self.env.context.get("scheduled") + try: + data_file = self._adyen_get_settlement_details_file() + import_wizard = self.env["account.bank.statement.import"].create( + { + "attachment_ids": [ + (0, 0, {"name": "test file", "datas": data_file}) + ] + } + ) + import_wizard.with_context( + {"account_bank_statement_import_adyen": True} + ).import_file() + except BaseException as e: + if is_scheduled: + _logger.warning( + 'Online Bank Statement Provider "%s" failed to' + " obtain statement data" % (provider.name,), + exc_info=True, + ) + provider.message_post( + body=_( + "Failed to obtain statement data for period " + ": %s. See server logs for more details." + ) + % (escape(str(e)) or _("N/A"),), + subject=_("Issue with Online Bank Statement Provider"), + ) + break + raise + if is_scheduled: + provider._schedule_next_run() + + def _adyen_get_settlement_details_file(self): + """Retrieve daily generated settlement details file. + + The file could be retrieved with wget using: + $ wget \ + --http-user='[YourReportUser]@Company.[YourCompanyAccount]' \ + --http-password='[YourReportUserPassword]' \ + --quiet --no-check-certificate \ + https://ca-test.adyen.com/reports/download/MerchantAccount/ + + [YourMerchantAccount]/[ReportFileName]" + """ + batch_number = self.next_batch_number + download_file_name = self.download_file_name % batch_number + URL = "/".join( + [self.api_base, self.journal_id.adyen_merchant_account, download_file_name] + ) + response = requests.get(URL, auth=(self.username, self.password)) + if response.status_code == 200: + return response.content + else: + raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) + + def _schedule_next_run(self): + """Set next run date and autoincrement batch number.""" + super()._schedule_next_run() + self.next_batch_number += 1 diff --git a/account_bank_statement_import_online_adyen/readme/CONFIGURE.rst b/account_bank_statement_import_online_adyen/readme/CONFIGURE.rst new file mode 100644 index 000000000..da06dd2c9 --- /dev/null +++ b/account_bank_statement_import_online_adyen/readme/CONFIGURE.rst @@ -0,0 +1,27 @@ +To configure online bank statements provider: + +#. Go to *Invoicing > Configuration > Bank Accounts* +#. Open bank account to configure and edit it +#. Set *Bank Feeds* to *Online* +#. Select *Adyen* as online bank statements provider in + *Online Bank Statements (OCA)* section +#. Save the bank account +#. Click on provider and configure provider-specific settings. + +or, alternatively: + +#. Go to *Invoicing > Overview* +#. Open settings of the corresponding journal account +#. Switch to *Bank Account* tab +#. Set *Bank Feeds* to *Online* +#. Select *Adyen* as online bank statements provider in + *Online Bank Statements (OCA)* section +#. Save the bank account +#. Click on provider and configure provider-specific settings. + +To obtain *Login* and *Key*: + +#. Open `Adyen website `_. + +Check also ``account_bank_statement_import_online`` configuration instructions +for more information. diff --git a/account_bank_statement_import_online_adyen/readme/CONTRIBUTORS.rst b/account_bank_statement_import_online_adyen/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..6ee4d1d63 --- /dev/null +++ b/account_bank_statement_import_online_adyen/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Ronald Portier - Therp BV diff --git a/account_bank_statement_import_online_adyen/readme/DESCRIPTION.rst b/account_bank_statement_import_online_adyen/readme/DESCRIPTION.rst new file mode 100644 index 000000000..fc01c0c4b --- /dev/null +++ b/account_bank_statement_import_online_adyen/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module automates the download and import of Adyen payment reports. diff --git a/account_bank_statement_import_online_adyen/readme/USAGE.rst b/account_bank_statement_import_online_adyen/readme/USAGE.rst new file mode 100644 index 000000000..2785a201a --- /dev/null +++ b/account_bank_statement_import_online_adyen/readme/USAGE.rst @@ -0,0 +1,10 @@ +To pull historical bank statements: + +#. Go to *Invoicing > Configuration > Bank Accounts* +#. Select specific bank accounts +#. Launch *Actions > Online Bank Statements Pull Wizard* +#. Configure date interval and click *Pull* + +If historical data is not needed, then just simply wait for the scheduled +activity "Pull Online Bank Statements" to be executed for getting new +transactions. diff --git a/account_bank_statement_import_online_adyen/static/description/icon.png b/account_bank_statement_import_online_adyen/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..09847ed7696bcad441aac277011add9a9a82c01d GIT binary patch literal 11070 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 literal 0 HcmV?d00001 diff --git a/account_bank_statement_import_online_adyen/static/description/index.html b/account_bank_statement_import_online_adyen/static/description/index.html new file mode 100644 index 000000000..beccd682f --- /dev/null +++ b/account_bank_statement_import_online_adyen/static/description/index.html @@ -0,0 +1,464 @@ + + + + + + +Online Bank Statements: Adyen payment report + + + +
+

Online Bank Statements: Adyen payment report

+ + +

Beta License: AGPL-3 OCA/bank-statement-import Translate me on Weblate Try me on Runbot

+

This module automates the download and import of Adyen payment reports.

+

Table of contents

+ +
+

Configuration

+

To configure online bank statements provider:

+
    +
  1. Go to Invoicing > Configuration > Bank Accounts
  2. +
  3. Open bank account to configure and edit it
  4. +
  5. Set Bank Feeds to Online
  6. +
  7. Select Adyen as online bank statements provider in +Online Bank Statements (OCA) section
  8. +
  9. Save the bank account
  10. +
  11. Click on provider and configure provider-specific settings.
  12. +
+

or, alternatively:

+
    +
  1. Go to Invoicing > Overview
  2. +
  3. Open settings of the corresponding journal account
  4. +
  5. Switch to Bank Account tab
  6. +
  7. Set Bank Feeds to Online
  8. +
  9. Select Adyen as online bank statements provider in +Online Bank Statements (OCA) section
  10. +
  11. Save the bank account
  12. +
  13. Click on provider and configure provider-specific settings.
  14. +
+

To obtain Login and Key:

+
    +
  1. Open Adyen website.
  2. +
+

Check also account_bank_statement_import_online configuration instructions +for more information.

+
+
+

Usage

+

To pull historical bank statements:

+
    +
  1. Go to Invoicing > Configuration > Bank Accounts
  2. +
  3. Select specific bank accounts
  4. +
  5. Launch Actions > Online Bank Statements Pull Wizard
  6. +
  7. Configure date interval and click Pull
  8. +
+

If historical data is not needed, then just simply wait for the scheduled +activity “Pull Online Bank Statements” to be executed for getting new +transactions.

+
+
+

Bug Tracker

+

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 smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Ronald Portier (Therp BV)
  • +
+
+
+

Contributors

+
    +
  • Ronald Portier - Therp BV
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/bank-statement-import project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_bank_statement_import_online_adyen/tests/__init__.py b/account_bank_statement_import_online_adyen/tests/__init__.py new file mode 100644 index 000000000..e889bae25 --- /dev/null +++ b/account_bank_statement_import_online_adyen/tests/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import test_import_online diff --git a/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py new file mode 100644 index 000000000..9c42c0aab --- /dev/null +++ b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py @@ -0,0 +1,23 @@ +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import base64 + +from odoo import models +from odoo.modules.module import get_module_resource + + +class OnlineBankStatementProviderDummy(models.Model): + _inherit = "online.bank.statement.provider" + + def _adyen_get_settlement_details_file(self): + """Get file from disk, instead of from url.""" + if self.service != "dummy_adyen": + # Not a dummy, get the regular adyen method. + return super()._adyen_get_settlement_details_file() + testfile = get_module_resource( + "account_bank_statement_import_adyen", "test_files", self.download_file_name + ) + with open(testfile, "rb") as datafile: + data_file = datafile.read() + data_file = base64.b64encode(data_file) + return data_file diff --git a/account_bank_statement_import_online_adyen/tests/test_import_online.py b/account_bank_statement_import_online_adyen/tests/test_import_online.py new file mode 100644 index 000000000..71c0d7653 --- /dev/null +++ b/account_bank_statement_import_online_adyen/tests/test_import_online.py @@ -0,0 +1,66 @@ +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from dateutil.relativedelta import relativedelta + +from odoo import fields + +from odoo.addons.account_bank_statement_import_adyen.tests.test_import_adyen import ( + TestImportAdyen, +) + + +class TestImportOnline(TestImportAdyen): + """Do the same tests as with the offline adyen import.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.now = fields.Datetime.now() + cls.journal.write( + { + "bank_statements_source": "online", + "online_bank_statement_provider": "dummy_adyen", + } + ) + + def test_03_import_adyen_invalid(self): + """Override super test: online module test will return without statements.""" + with self.assertRaisesRegex(AssertionError, "account.bank.statement()"): + self._test_statement_import( + "adyen_test_invalid.xls", "invalid", + ) + + def _test_statement_import(self, file_name, statement_name): + """Test correct creation of single statement. + + Getting an adyen statement online should result in: + 1. A valid imported statement; + 2. An incremented batch number; + 3. The current date being set as the date_since in the provider. + """ + provider = self.journal.online_bank_statement_provider_id + provider.write( + { + "api_base": ( + "https://ca-test.adyen.com/reports/download/MerchantAccount" + ), + "download_file_name": file_name, + "interval_type": "days", + "interval_number": 1, + "service": "dummy_adyen", + "next_batch_number": 1, + } + ) + # Pull from yesterday, until today + yesterday = self.now - relativedelta(days=1) + provider.with_context(scheduled=True)._pull(yesterday, self.now) + # statement name is account number + '-' + date of last line: + statements = self.env["account.bank.statement"].search( + [("name", "=", statement_name)] + ) + self.assertTrue(statements) + self.assertEqual(len(statements), 1) + self.assertEqual(provider.next_batch_number, 2) + self.assertEqual(provider.last_successful_run, self.now) + self.assertTrue(provider.next_run > self.now) + return statements diff --git a/account_bank_statement_import_online_adyen/views/online_bank_statement_provider.xml b/account_bank_statement_import_online_adyen/views/online_bank_statement_provider.xml new file mode 100644 index 000000000..294c77690 --- /dev/null +++ b/account_bank_statement_import_online_adyen/views/online_bank_statement_provider.xml @@ -0,0 +1,28 @@ + + + + online.bank.statement.provider.form + online.bank.statement.provider + + + + + + + + + + + + + + From 6dee1362f4c7d59ed46bbde6375c14f50e49b685 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Tue, 30 Mar 2021 11:05:06 +0200 Subject: [PATCH 09/24] [FIX] absi- _paypal Module did not properly check input. If passed any csv file, paypal module would try to use it, even if clearly not a paypal file. --- .../models/account_bank_statement_import_paypal_parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py b/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py index 2a6f323d9..395ff7065 100644 --- a/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py +++ b/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py @@ -110,6 +110,8 @@ def _parse_lines(self, mapping, data_file, currency_code): header = list(next(csv_data)) data_dict = self._data_dict_constructor(mapping, header) + if data_dict.get("currency_column") is None: + raise ValueError(_("No currency column, not a valid Paypal file")) return self._calculate_lines(csv_data, data_dict, mapping, currency_code) From 5bc308657c82f740b0f7937736ffb528c6d25f1a Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 17 Mar 2021 13:28:41 +0100 Subject: [PATCH 10/24] [FIX] absi- _transfer_move. Correct test and duplicate class name --- .../models/account_bank_statement_import.py | 33 ++++++++- .../models/account_journal.py | 2 +- .../tests/test_statement.py | 69 ++++--------------- 3 files changed, 46 insertions(+), 58 deletions(-) diff --git a/account_bank_statement_import_transfer_move/models/account_bank_statement_import.py b/account_bank_statement_import_transfer_move/models/account_bank_statement_import.py index af145c466..e2ae67398 100644 --- a/account_bank_statement_import_transfer_move/models/account_bank_statement_import.py +++ b/account_bank_statement_import_transfer_move/models/account_bank_statement_import.py @@ -1,13 +1,44 @@ # Copyright 2020 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import models +from odoo import api, models class AccountBankStatementImport(models.TransientModel): _inherit = "account.bank.statement.import" + @api.model + def _parse_file(self, data_file): + """Enable testing of this functionality.""" + if self.env.context.get("account_bank_statement_import_transfer_move", False): + return ( + None, + "NL77ABNA0574908765", + [ + { + "balance_end_real": 15121.12, + "balance_start": 15568.27, + "date": "2014-01-05", + "name": "1234Test/1", + "transactions": [ + { + "account_number": "NL46ABNA0499998748", + "amount": -754.25, + "date": "2014-01-05", + "name": "Insurance policy 857239PERIOD 01.01.2014 - " + "31.12.2014", + "note": "MKB Insurance 859239PERIOD 01.01.2014 - " + "31.12.2014", + "partner_name": "INSURANCE COMPANY TESTX", + "ref": "435005714488-ABNO33052620", + }, + ], + } + ], + ) + return super()._parse_file(data_file) + def _create_bank_statements(self, stmts_vals): """ Create additional line in statement to set bank statement statement to 0 balance""" diff --git a/account_bank_statement_import_transfer_move/models/account_journal.py b/account_bank_statement_import_transfer_move/models/account_journal.py index 6bd9612a0..92bcd270b 100644 --- a/account_bank_statement_import_transfer_move/models/account_journal.py +++ b/account_bank_statement_import_transfer_move/models/account_journal.py @@ -4,7 +4,7 @@ from odoo import fields, models -class AccountBankStatementImport(models.Model): +class AccountJournal(models.Model): _inherit = "account.journal" diff --git a/account_bank_statement_import_transfer_move/tests/test_statement.py b/account_bank_statement_import_transfer_move/tests/test_statement.py index e81b7856d..1c6ba3d8f 100644 --- a/account_bank_statement_import_transfer_move/tests/test_statement.py +++ b/account_bank_statement_import_transfer_move/tests/test_statement.py @@ -1,8 +1,6 @@ # Copyright 2020 Camptocamp SA # Copyright 2020 Tecnativa - Pedro M. Baeza -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from unittest.mock import patch - +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo.tests.common import SavepointCase @@ -35,60 +33,19 @@ def setUpClass(cls): } ) - def _parse_file(self, data_file): - """Fake method for returning valuable data. Extracted from CAMT demo""" - return ( - None, - "NL77ABNA0574908765", - [ - { - "balance_end_real": 15121.12, - "balance_start": 15568.27, - "date": "2014-01-05", - "name": "1234Test/1", - "transactions": [ - { - "account_number": "NL46ABNA0499998748", - "amount": -754.25, - "date": "2014-01-05", - "name": "Insurance policy 857239PERIOD 01.01.2014 - " - "31.12.2014", - "note": "MKB Insurance 859239PERIOD 01.01.2014 - " - "31.12.2014", - "partner_name": "INSURANCE COMPANY TESTX", - "ref": "435005714488-ABNO33052620", - }, - ], - } - ], - ) - - def _get_bank_statements_available_import_formats(self): - """Fake method for returning a fake importer for not having errors.""" - return ["test"] - def _load_statement(self): - module = "odoo.addons.account_bank_statement_import" - with patch( - module - + ".account_journal.AccountJournal" - + "._get_bank_statements_available_import_formats", - self._get_bank_statements_available_import_formats, - ): - with patch( - module - + ".account_bank_statement_import" - + ".AccountBankStatementImport._parse_file", - self._parse_file, - ): - self.env["account.bank.statement.import"].create( - {"attachment_ids": [(0, 0, {"name": "test file", "datas": b""})]} - ).import_file() - bank_st_record = self.env["account.bank.statement"].search( - [("name", "=", "1234Test/1")], limit=1 - ) - statement_lines = bank_st_record.line_ids - return statement_lines + """Load fake statements, to test creation of extra line.""" + absi = self.env["account.bank.statement.import"].create( + {"attachment_ids": [(0, 0, {"name": "test file", "datas": b""})]} + ) + absi.with_context( + {"account_bank_statement_import_transfer_move": True} + ).import_file() + bank_st_record = self.env["account.bank.statement"].search( + [("name", "=", "1234Test/1")], limit=1 + ) + statement_lines = bank_st_record.line_ids + return statement_lines def test_statement_import(self): self.journal.transfer_line = True From 371d2bb9fbf7fc8aefeaf83f6b7f01da564c4716 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Mon, 29 Mar 2021 15:20:24 +0200 Subject: [PATCH 11/24] [IMP] absi- _adyen. Fully support csv files and reordered columns --- .../models/account_bank_statement_import.py | 253 ++++++++++++------ .../settlement_detail_report_batch_380.csv | 229 ++++++++++++++++ .../tests/test_import_adyen.py | 23 +- .../models/online_bank_statement_provider.py | 15 +- .../online_bank_statement_provider_dummy.py | 5 +- .../tests/test_import_online.py | 2 +- 6 files changed, 431 insertions(+), 96 deletions(-) create mode 100644 account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_380.csv 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 4df68c578..649d882ab 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 @@ -3,31 +3,58 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import logging -from odoo import _, api, fields, models +from odoo import _, fields, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) +COLUMNS = { + "Company Account": 1, + "Merchant Account": 2, + "Psp Reference": 3, + "Merchant Reference": 4, + "Payment Method": 5, # Not used at present + "Creation Date": 6, + "TimeZone": 7, # Not used at present + "Type": 8, + "Modification Reference": 9, + "Gross Currency": 10, # Not used at present + "Gross Debit (GC)": 11, # Not used at present + "Gross Credit (GC)": 12, # Not used at present + "Exchange Rate": 13, # Not used at present + "Net Currency": 14, + "Net Debit (NC)": 15, # Fee or Merchant Payout + "Net Credit (NC)": 16, + "Commission (NC)": 17, + "Markup (NC)": 18, + "Scheme Fees (NC)": 19, + "Interchange (NC)": 20, + "Payment Method Variant": 21, + "Modification Merchant Reference": 22, # Not used at present + "Batch Number": 23, + "Reserved4": 24, # Not used at present + "Reserved5": 25, # Not used at present + "Reserved6": 26, # Not used at present + "Reserved7": 27, # Not used at present + "Reserved8": 28, # Not used at present + "Reserved9": 29, # Not used at present + "Reserved10": 30, # Not used at present +} + class AccountBankStatementImport(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: - try: - return self._parse_adyen_file(data_file) - except Exception as exc: - if self.env.context.get("account_bank_statement_import_adyen", False): - raise - _logger.info("Adyen parser error", exc_info=True) - raise ValueError("Not an adyen settlements file: %s" % exc) - except ValueError: - _logger.debug( - _("Statement file was not a Adyen settlement details file."), - exc_info=True, - ) + _logger.debug(_("Try parsing as Adyen settlement details.")) + return self._parse_adyen_file(data_file) + except Exception: + message = _("Statement file was not a Adyen settlement details file.") + if self.env.context.get("account_bank_statement_import_adyen", False): + raise UserError(message) + _logger.debug(message, exc_info=True) return super()._parse_file(data_file) def _find_additional_data(self, currency_code, account_number): @@ -49,95 +76,48 @@ def _find_additional_data(self, currency_code, account_number): self = self.with_context(journal_id=journal.id) return super()._find_additional_data(currency_code, account_number) - @api.model - def _balance(self, row): - return -(float(row[15]) if row[15] else 0.0) + sum( - float(row[i]) if row[i] else 0.0 for i in (16, 17, 18, 19, 20) - ) - - @api.model - 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="{} {} {} {}".format(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 _parse_adyen_file(self, data_file): - statements = [] + """Parse file assuming it is an Adyen file. + + An Excception will be thrown if file cannot be parsed. + """ statement = None headers = False fees = 0.0 balance = 0.0 payout = 0.0 - statement_id = None - - import_model = self.env["base_import.import"] - importer = import_model.create( - {"file": data_file, "file_name": "Ayden settlemnt details"} - import_model = self.env["base_import.import"] - importer = import_model.create( - {"file": data_file, "file_name": "Ayden settlement details"} - ) - rows = importer._read_file({}) - + rows = self._get_rows(data_file) for row in rows: - if len(row) != 31: + if len(row) < 24: raise ValueError( "Not an Adyen statement. Unexpected row length %s " - "instead of 31" % len(row) + "less then minimum of 24" % 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], - ) + self._set_columns(row) headers = True continue if not statement: - statement = {"transactions": []} - statements.append(statement) - statement_id = "{merchant} {year}/{batch}".format( - merchant=row[2], year=row[6][:4], batch=row[23], - ) - currency_code = row[14] - merchant_id = row[2] - statement["name"] = statement_id - 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": + statement = self._make_statement(row) + currency_code = self._get_value(row, "Net Currency") + merchant_id = self._get_value(row, "Merchant Account") + else: + self._update_statement(statement, row) + row_type = self._get_value(row, "Type").strip() + if row_type == "MerchantPayout": payout -= self._balance(row) else: balance += self._balance(row) - self._import_adyen_transaction(statement, statement_id, row) - fees += sum(float(row[i]) if row[i] else 0.0 for i in (17, 18, 19, 20)) + self._import_adyen_transaction(statement, row) + fees += self._sum_fees(row) if not headers: raise ValueError("Not an Adyen statement. Did not encounter header row.") - if 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 - statement["transactions"].append(transaction) - + self._add_fees_transaction(statement, fees, row) 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: @@ -145,4 +125,117 @@ def _parse_adyen_file(self, data_file): _("Parse error. Balance %s not equal to merchant " "payout %s") % (balance, payout) ) - return currency_code, merchant_id, statements + return currency_code, merchant_id, [statement] + + def _get_rows(self, data_file): + """Get rows from data_file.""" + # Try to use original import file name. + filename = ( + self.attachment_ids[0].name + if len(self.attachment_ids) == 1 + else "Ayden settlement details" + ) + import_model = self.env["base_import.import"] + importer = import_model.create({"file": data_file, "file_name": filename}) + return importer._read_file({"quoting": '"', "separator": ","}) + + def _set_columns(self, row): + """Set columns from headers. There MUST be a 'Company Account' header.""" + seen_company_account = False + for num, header in enumerate(row): + if header == "Company Account": + seen_company_account = True + if header not in COLUMNS: + _logger.debug(_("Unknown header %s in Adyen statement headers"), header) + else: + COLUMNS[header] = num # Set the right number for the column. + if not seen_company_account: + raise ValueError( + _("Not an Adyen statement. Headers %s do not contain 'Company Account'") + % ", ".join(row) + ) + + def _get_value(self, row, column): + """Get the value from the righ column in the row.""" + return row[COLUMNS[column]] + + def _make_statement(self, row): + """Make statement on first transaction in file.""" + statement = {"transactions": []} + statement["name"] = "{merchant} {year}/{batch}".format( + merchant=self._get_value(row, "Merchant Account"), + year=self._get_value(row, "Creation Date")[:4], + batch=self._get_value(row, "Batch Number"), + ) + statement["date"] = self._get_transaction_date(row) + return statement + + def _get_transaction_date(self, row): + """Get transaction date in right format.""" + return fields.Date.from_string(self._get_value(row, "Creation Date")) + + def _update_statement(self, statement, row): + """Update statement from transaction row.""" + # Statement date is date of earliest transaction in file. + date = self._get_transaction_date(row) + if date < statement.get("date"): + statement["date"] = date + + def _balance(self, row): + return ( + -self._sum_amount_values(row, ("Net Debit (NC)",)) + + self._sum_amount_values(row, ("Net Credit (NC)",)) + + self._sum_fees(row) + ) + + def _sum_fees(self, row): + """Sum the amounts in the fees columns.""" + return self._sum_amount_values( + row, + ("Commission (NC)", "Markup (NC)", "Scheme Fees (NC)", "Interchange (NC)",), + ) + + def _sum_amount_values(self, row, columns): + """Sum the amounts from the columns passed.""" + amount = 0.0 + for column in columns: + value = self._get_value(row, column) + if value: + amount += float(value) + return amount + + def _import_adyen_transaction(self, statement, row): + """Add transaction from row to statements.""" + transaction = dict( + unique_import_id=self._get_unique_import_id(statement), + date=self._get_transaction_date(row), + amount=self._balance(row), + note="{} {} {} {}".format( + self._get_value(row, "Merchant Account"), + self._get_value(row, "Psp Reference"), + self._get_value(row, "Merchant Reference"), + self._get_value(row, "Payment Method Variant"), + ), + name="%s" + % ( + self._get_value(row, "Psp Reference") + or self._get_value(row, "Merchant Reference") + or self._get_value(row, "Modification Reference") + ), + ) + statement["transactions"].append(transaction) + + def _get_unique_import_id(self, statement): + """get unique import ID for transaction.""" + return statement["name"] + str(len(statement["transactions"])).zfill(4) + + def _add_fees_transaction(self, statement, fees, row): + """Single transaction for all fees in statement.""" + transaction = dict( + unique_import_id=self._get_unique_import_id(statement), + date=max(t["date"] for t in statement["transactions"]), + amount=-fees, + name="Commission, markup etc. batch %s" + % self._get_value(row, "Batch Number"), + ) + statement["transactions"].append(transaction) diff --git a/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_380.csv b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_380.csv new file mode 100644 index 000000000..ff1301666 --- /dev/null +++ b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_380.csv @@ -0,0 +1,229 @@ +Company Account,Merchant Account,Psp Reference,Merchant Reference,Payment Method,Creation Date,TimeZone,Type,Modification Reference,Gross Currency,Gross Debit (GC),Gross Credit (GC),Exchange Rate,Net Currency,Net Debit (NC),Net Credit (NC),Commission (NC),Markup (NC),Scheme Fees (NC),Interchange (NC),Payment Method Variant,Batch Number,Reserved4,Reserved5,Reserved6,Reserved7,Reserved8,Reserved9,Reserved10,Modification Merchant Reference,Booking Date,Booking Date TimeZone +CompanyNL,YOURCOMPANY_ACCOUNT,1829098024817852,CM1000028854,mc,2021-01-05 00:21:32,CET,Settled,7429098024927090,USD,,31.45,1,USD,,31.15,,0.16,0.05,0.09,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4636098029442802,CM2000162856,visa,2021-01-05 00:29:21,CET,Settled,7936098029610473,USD,,21.95,1,USD,,21.74,,0.11,0.03,0.07,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1736098031315223,CM7000054248,visa,2021-01-05 00:32:20,CET,Settled,7436098031406030,USD,,26.45,1,USD,,26.2,,0.14,0.03,0.08,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098037405950,CM3000151362,directEbanking,2021-01-05 00:43:27,CET,Settled,1316098037405950,USD,,96.9,1,USD,,95.93,0.97,,,,directEbanking,380,,,,,,,,CM3000151362,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1656098094611035,CM2000162861,mc,2021-01-05 02:18:53,CET,Settled,7759098095331123,USD,,589.75,1,USD,,584.48,,3.07,1.02,1.18,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098263305981,CM2000162863,ideal,2021-01-05 06:59:37,CET,Settled,1316098263305981,USD,,26.45,1,USD,,26.2,0.25,,,,idealtriodos,380,,,,,,,,CM2000162863,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6119098279552327,CM2000162864,ideal,2021-01-05 07:26:28,CET,Settled,6119098279552327,USD,,116.3,1,USD,,116.05,0.25,,,,idealing,380,,,,,,,,CM2000162864,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098280206459,CM1000028855,ideal,2021-01-05 07:27:26,CET,Settled,1516098280206459,USD,,26.45,1,USD,,26.2,0.25,,,,idealasn,380,,,,,,,,CM1000028855,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098288495570,CM7000054252,cartebancaire,2021-01-05 07:42:17,CET,Settled,2319098288620641,USD,,24.95,1,USD,,24.72,,0.15,0.01,0.07,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098301438837,CM7000054253,cartebancaire,2021-01-05 08:04:27,CET,Settled,5316098302004588,USD,,119.75,1,USD,,118.78,,0.72,0.01,0.24,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098304031443,CM1000028856,ideal,2021-01-05 08:07:05,CET,Settled,1816098304031443,USD,,66.45,1,USD,,66.2,0.25,,,,idealasn,380,,,,,,,,CM1000028856,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219098308593440,CM7000054254,cartebancaire,2021-01-05 08:16:46,CET,Settled,2416098309442597,USD,,79.85,1,USD,,79.2,,0.48,0.01,0.16,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619093887226154,CM7000053968,bankTransfer_IBAN,2021-01-05 08:25:01,CET,Settled,1619093887226154,USD,,518.9,1,USD,,518.7,0.2,,,,bankTransfer_IBAN,380,,,,,,,,CM7000053968,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4316095884890882,CM1000028786,bankTransfer_NL,2021-01-05 08:25:18,CET,Settled,4316095884890882,USD,,1044.35,1,USD,,1044.15,0.2,,,,bankTransfer_NL,380,,,,,,,,CM1000028786,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098318146617,CM1000028857,ideal,2021-01-05 08:30:59,CET,Settled,1816098318146617,USD,,66.45,1,USD,,66.2,0.25,,,,idealing,380,,,,,,,,CM1000028857,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4556098318416786,CM2000162866,visa,2021-01-05 08:32:58,CET,Settled,7659098319786491,USD,,81.85,1,USD,,81.22,,0.43,0.04,0.16,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098325040696,CM1000028858,ideal,2021-01-05 08:42:48,CET,Settled,1816098325040696,USD,,26.45,1,USD,,26.2,0.25,,,,idealasn,380,,,,,,,,CM1000028858,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1559098334575279,CM1000028859,bcmc_mobile,2021-01-05 08:57:39,CET,Settled,7759098334591982,USD,,26.45,1,USD,,26.22,,0.16,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6239098336305763,CM2000162873,visa,2021-01-05 09:02:04,CET,Settled,7936098337243280,USD,,565.8,1,USD,,561.55,,2.94,0.18,1.13,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1359098338215734,CM18000001445,visa,2021-01-05 09:04:16,CET,Settled,7759098338563615,USD,,50.12,1,USD,,48.91,,0.26,0.2,0.75,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1539098339357892,CM3000151370,visa,2021-01-05 09:05:38,CET,Settled,7439098339386626,USD,,30.9,1,USD,,30.12,,0.16,0.16,0.46,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1339098339577947,CM7000054255,visa,2021-01-05 09:06:05,CET,Settled,7439098339659113,USD,,49.85,1,USD,,49.46,,0.26,0.03,0.1,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1536098341845648,CM1000028860,visa,2021-01-05 09:11:00,CET,Settled,7936098342605952,USD,,555.35,1,USD,,550.51,,2.89,0.28,1.67,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1616098342598462,CM3000151371,directEbanking,2021-01-05 09:12:24,CET,Settled,1616098342598462,USD,,555.8,1,USD,,550.24,5.56,,,,directEbanking,380,,,,,,,,CM3000151371,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4646098344291577,CM17000002640,mc,2021-01-05 09:13:55,CET,Settled,7649098344354771,USD,,27.08,1,USD,,26.44,,0.14,0.09,0.41,mcsuperpremiumcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4726098345327373,CM7000054256,mc,2021-01-05 09:15:47,CET,Settled,7926098345475009,USD,,46.4,1,USD,,45.95,,0.24,0.07,0.14,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1856098346315573,CM2000162874,visa,2021-01-05 09:19:29,CET,Settled,7759098347699640,USD,,588.75,1,USD,,584.33,,3.06,0.18,1.18,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4326098349237309,CM3000151372,visa,2021-01-05 09:28:08,CET,Settled,7429098352883956,USD,,475.95,1,USD,,471.8,,2.47,0.25,1.43,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1526098355393578,CM7000054259,visa,2021-01-05 09:33:49,CET,Settled,7426098356298725,USD,,518.9,1,USD,,508.41,,2.7,0.27,7.52,visacommercialcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1726098357414842,CM7000054260,bcmc_mobile,2021-01-05 09:35:48,CET,Settled,7426098357485040,USD,,156.3,1,USD,,155.29,,0.94,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098357756292,CM3000151374,giropay,2021-01-05 09:39:00,CET,Settled,1816098357756292,USD,,46.85,1,USD,,46.04,0.81,,,,giropay,380,,,,,,,,CM3000151374,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098360214266,CM1000028861,ideal,2021-01-05 09:41:03,CET,Settled,1516098360214266,USD,,36.45,1,USD,,36.2,0.25,,,,idealasn,380,,,,,,,,CM1000028861,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219098360158827,CM3000151377,giropay,2021-01-05 09:43:16,CET,Settled,6219098360158827,USD,,36.9,1,USD,,36.22,0.68,,,,giropay,380,,,,,,,,CM3000151377,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4336098363026289,CM2000162876,visa,2021-01-05 09:45:05,CET,Settled,7936098363059485,USD,,34.9,1,USD,,34.62,,0.18,0.03,0.07,electron,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098365115461,CM2000162878,cartebancaire,2021-01-05 09:49:53,CET,Settled,7916098365186579,USD,,46.45,1,USD,,46.07,,0.28,0.01,0.09,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098368800567,CM7000054262,cartebancaire,2021-01-05 09:55:45,CET,Settled,7419098368870510,USD,,46.85,1,USD,,46.47,,0.28,0.01,0.09,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098372030965,CM1000028862,ideal,2021-01-05 10:00:57,CET,Settled,1516098372030965,USD,,46.4,1,USD,,46.15,0.25,,,,idealing,380,,,,,,,,CM1000028862,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1846098379029995,CM2000162881,visa,2021-01-05 10:11:50,CET,Settled,7549098379106006,USD,,31.9,1,USD,,31.6,,0.17,0.03,0.1,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4636098378101900,CM2000162880,visa,2021-01-05 10:12:27,CET,Settled,7936098379470528,USD,,529.85,1,USD,,525.86,,2.76,0.17,1.06,visasuperpremiumdebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4646080478327021,CM3000149254,visa,2021-01-05 10:17:31,CET,Refunded,4346098382059723,USD,19.95,,1,USD,19.95,,,,,,visastandardcredit,380,,,,,,,,CM3000149254,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1549098388405186,CM17000002641,visa,2021-01-05 10:27:52,CET,Settled,7749098388722759,USD,,487.6,1,USD,,476.46,,2.54,1.29,7.31,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1549098390299692,CM7000054263,visa,2021-01-05 10:31:52,CET,Settled,7749098391124118,USD,,538.85,1,USD,,534.8,,2.8,0.17,1.08,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098392069723,CM7000054264,cartebancaire,2021-01-05 10:34:57,CET,Settled,7916098392205897,USD,,29.9,1,USD,,29.65,,0.18,0.01,0.06,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098401930718,CM2000162887,ideal,2021-01-05 10:50:48,CET,Settled,4516098401930718,USD,,66.35,1,USD,,66.1,0.25,,,,idealrabobank,380,,,,,,,,CM2000162887,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6239098403242287,CM7000054268,visa,2021-01-05 10:52:47,CET,Settled,7439098403677975,USD,,483.72,1,USD,,472.97,,2.52,0.25,7.98,visacorporatecredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4316098407246061,CM1000028863,ideal,2021-01-05 10:59:17,CET,Settled,4316098407246061,USD,,6.5,1,USD,,6.25,0.25,,,,idealing,380,,,,,,,,CM1000028863,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4326098409899006,CM3000151391,visa,2021-01-05 11:03:26,CET,Settled,7426098410064121,USD,,141.95,1,USD,,140.7,,0.74,0.08,0.43,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1359098412271835,CM2000162890,mc,2021-01-05 11:07:34,CET,Settled,7759098412548011,USD,,181.75,1,USD,,180.1,,0.95,0.15,0.55,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1539098414025232,CM7000054274,visa,2021-01-05 11:10:17,CET,Settled,7439098414172511,USD,,34.95,1,USD,,34.64,,0.18,0.03,0.1,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1556098414930348,CM7000054275,visa,2021-01-05 11:12:29,CET,Settled,7559098415497986,USD,,478.95,1,USD,,475.35,,2.49,0.15,0.96,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4536098415344183,CM7000054276,mc,2021-01-05 11:13:54,CET,Settled,7439098416347798,USD,,478.95,1,USD,,474.64,,2.49,0.38,1.44,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1849098416334181,CM7000054277,mc,2021-01-05 11:14:33,CET,Settled,7549098416733952,USD,,79.85,1,USD,,79.2,,0.42,0.07,0.16,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1336098417118690,CM2000162891,mc,2021-01-05 11:15:31,CET,Settled,7436098417314824,USD,,69.85,1,USD,,69.22,,0.36,0.06,0.21,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4756098417302497,CM2000162892,visa,2021-01-05 11:15:46,CET,Settled,7659098417466145,USD,,31.9,1,USD,,31.64,,0.17,0.03,0.06,electron,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6249098418547686,CM3000151393,visa,2021-01-05 11:17:48,CET,Settled,7649098418680812,USD,,36.95,1,USD,,36.62,,0.19,0.03,0.11,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098421158771,CM3000151394,directEbanking,2021-01-05 11:23:00,CET,Settled,1619098421158771,USD,,475.95,1,USD,,471.19,4.76,,,,directEbanking,380,,,,,,,,CM3000151394,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098424350301,CM1000028864,ideal,2021-01-05 11:32:29,CET,Settled,1816098424350301,USD,,31.5,1,USD,,31.25,0.25,,,,idealtriodos,380,,,,,,,,CM1000028864,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4746098428746750,CM15000004736,visa,2021-01-05 11:35:45,CET,Settled,7749098429453067,USD,,430.95,1,USD,,427.71,,2.24,0.14,0.86,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098432126040,CM3000151396,directEbanking,2021-01-05 11:41:32,CET,Settled,1519098432126040,USD,,555.8,1,USD,,550.24,5.56,,,,directEbanking,380,,,,,,,,CM3000151396,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098431938133,CM3000151395,directEbanking,2021-01-05 11:44:06,CET,Settled,1516098431938133,USD,,25.76,1,USD,,25.5,0.26,,,,directEbanking,380,,,,,,,,CM3000151395,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1546098438298972,CM7000054278,visa,2021-01-05 11:50:32,CET,Settled,7549098438328351,USD,,29.9,1,USD,,29.65,,0.16,0.03,0.06,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1336098447468164,CM3000151400,visa,2021-01-05 12:06:23,CET,Settled,7439098447832487,USD,,515.9,1,USD,,512.03,,2.68,0.16,1.03,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716098449607813,CM1000028865,ideal,2021-01-05 12:09:58,CET,Settled,4716098449607813,USD,,66.45,1,USD,,66.2,0.25,,,,idealasn,380,,,,,,,,CM1000028865,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1649098450600333,CM7000054279,visa,2021-01-05 12:11:47,CET,Settled,7649098451071325,USD,,518.9,1,USD,,515,,2.7,0.16,1.04,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1336098451682403,CM3000151401,visa,2021-01-05 12:14:02,CET,Settled,7436098452422094,USD,,475.95,1,USD,,472.38,,2.47,0.15,0.95,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098456224257,CM1000028866,ideal,2021-01-05 12:20:53,CET,Settled,1719098456224257,USD,,26.45,1,USD,,26.2,0.25,,,,idealrabobank,380,,,,,,,,CM1000028866,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4746098458272701,CM7000054280,mc,2021-01-05 12:29:11,CET,Settled,7649098461512151,USD,,548.85,1,USD,,544.47,,2.85,0.43,1.1,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098461703368,CM1000028867,ideal,2021-01-05 12:31:21,CET,Settled,4616098461703368,USD,,66.45,1,USD,,66.2,0.25,,,,idealtriodos,380,,,,,,,,CM1000028867,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1749098464912244,CM2000162894,mc,2021-01-05 12:34:56,CET,Settled,7749098464964093,USD,,6.95,1,USD,,6.89,,0.04,0.01,0.01,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1756098464186658,CM20000002451,mc,2021-01-05 12:35:13,CET,Settled,7559098465135312,USD,,561.85,1,USD,,556.8,,2.92,0.44,1.69,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1756098478581449,CM2000162895,visa,2021-01-05 12:57:49,CET,Settled,7559098478696391,USD,,26.45,1,USD,,26.2,,0.14,0.03,0.08,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1529098480373559,CM2000162897,visa,2021-01-05 13:01:40,CET,Settled,7429098481008681,USD,,535.4,1,USD,,530.74,,2.78,0.27,1.61,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816098481304518,CM1000028869,ideal,2021-01-05 13:03:17,CET,Settled,4816098481304518,USD,,515.45,1,USD,,515.2,0.25,,,,idealrabobank,380,,,,,,,,CM1000028869,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4556098484634014,CM7000054282,mc,2021-01-05 13:08:07,CET,Settled,7759098484873909,USD,,468.85,1,USD,,465.24,,2.44,0.23,0.94,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098493524448,CM3000151406,directEbanking,2021-01-05 13:23:37,CET,Settled,1519098493524448,USD,,26.9,1,USD,,26.63,0.27,,,,directEbanking,380,,,,,,,,CM3000151406,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4536098501532824,CM3000151407,visa,2021-01-05 13:35:55,CET,Settled,7936098501550114,USD,,26.9,1,USD,,26.65,,0.14,0.03,0.08,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1346098502166016,CM15000004737,visa,2021-01-05 13:37:28,CET,Settled,7549098502485178,USD,,51.95,1,USD,,51.55,,0.27,0.03,0.1,electron,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098503744553,CM3000151408,directEbanking,2021-01-05 13:39:59,CET,Settled,4516098503744553,USD,,26.9,1,USD,,26.63,0.27,,,,directEbanking,380,,,,,,,,CM3000151408,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098504352119,CM7000054284,cartebancaire,2021-01-05 13:42:59,CET,Settled,7419098505011440,USD,,149.75,1,USD,,148.54,,0.9,0.01,0.3,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4646098509498355,CM7000054285,mc,2021-01-05 13:49:43,CET,Settled,7649098509837982,USD,,548.85,1,USD,,543.94,,2.85,0.96,1.1,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098518102027,CM7000054288,cartebancaire,2021-01-05 14:05:26,CET,Settled,2419098518485053,USD,,82.85,1,USD,,82.18,,0.5,,0.17,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098520371467,CM7000054289,cartebancaire,2021-01-05 14:09:36,CET,Settled,7916098520830012,USD,,448.9,1,USD,,444.86,,2.69,,1.35,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319098525213079,CM7000054290,cartebancaire,2021-01-05 14:17:08,CET,Settled,5416098525657484,USD,,591.75,1,USD,,587.01,,3.55,0.01,1.18,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1616098530359040,CM2000162903,directEbanking,2021-01-05 14:28:02,CET,Settled,1616098530359040,USD,,26.9,1,USD,,26.63,0.27,,,,directEbanking,380,,,,,,,,CM2000162903,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219098534473078,CM7000054291,cartebancaire,2021-01-05 14:33:16,CET,Settled,7916098534916500,USD,,69.95,1,USD,,69.38,,0.42,0.01,0.14,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1856098543755332,CM2000162908,visa,2021-01-05 14:47:13,CET,Settled,7559098544336766,USD,,545.4,1,USD,,541.3,,2.84,0.17,1.09,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098545972663,CM1000028870,ideal,2021-01-05 14:50:37,CET,Settled,1516098545972663,USD,,545.4,1,USD,,545.15,0.25,,,,idealing,380,,,,,,,,CM1000028870,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098546477771,CM1000028871,ideal,2021-01-05 14:51:41,CET,Settled,1316098546477771,USD,,31.45,1,USD,,31.2,0.25,,,,idealing,380,,,,,,,,CM1000028871,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716098547580789,CM1000028872,ideal,2021-01-05 14:55:11,CET,Settled,4716098547580789,USD,,585.3,1,USD,,585.05,0.25,,,,idealasn,380,,,,,,,,CM1000028872,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1359098553987017,CM2000162909,mc,2021-01-05 15:04:09,CET,Settled,7659098554493019,USD,,475.95,1,USD,,471.67,,2.47,0.38,1.43,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098558319300,CM7000054292,cartebancaire,2021-01-05 15:12:36,CET,Settled,2419098558828295,USD,,448.9,1,USD,,444.86,,2.69,,1.35,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4316098560947976,CM1000028873,ideal,2021-01-05 15:16:52,CET,Settled,4316098560947976,USD,,56.45,1,USD,,56.2,0.25,,,,idealtriodos,380,,,,,,,,CM1000028873,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1336098559877129,CM3000151416,visa,2021-01-05 15:17:04,CET,Settled,7936098562244607,USD,,68.84,1,USD,,68.3,,0.36,0.04,0.14,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098564691485,CM7000054293,cartebancaire,2021-01-05 15:22:18,CET,Settled,2416098564712379,USD,,29.9,1,USD,,29.65,,0.18,0.01,0.06,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4626098566791803,CM15000004738,mc,2021-01-05 15:25:39,CET,Settled,7426098567395492,USD,,430.95,1,USD,,427.51,,2.24,0.34,0.86,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1819098565442906,CM3000151418,directEbanking,2021-01-05 15:25:56,CET,Settled,1819098565442906,USD,,31.95,1,USD,,31.63,0.32,,,,directEbanking,380,,,,,,,,CM3000151418,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219098567337430,CM3000151420,directEbanking,2021-01-05 15:28:44,CET,Settled,6219098567337430,USD,,545.85,1,USD,,540.39,5.46,,,,directEbanking,380,,,,,,,,CM3000151420,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219098570269216,CM7000054294,cartebancaire,2021-01-05 15:31:34,CET,Settled,5316098570350937,USD,,24.95,1,USD,,24.72,,0.15,0.01,0.07,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1529098572249212,CM14000007552,mc,2021-01-05 15:34:04,CET,Settled,7429098572448465,USD,,60.9,1,USD,,60.43,,0.32,0.03,0.12,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4345895611165986,CM300122499,mc,2021-01-05 15:36:57,CET,Refunded,6249098573617340,USD,75,,1,USD,75,,,,,,mcstandardcredit,380,,,,,,,,CM300122499,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4846098575089755,CM7000054295,visa,2021-01-05 15:39:25,CET,Settled,7549098575651482,USD,,99.8,1,USD,,99.04,,0.52,0.04,0.2,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098584477539,CM7000054297,cartebancaire,2021-01-05 15:55:31,CET,Settled,7419098584590322,USD,,24.95,1,USD,,24.72,,0.15,0.01,0.07,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4756098584131257,CM3000151423,visa,2021-01-05 15:55:33,CET,Settled,7659098585336906,USD,,515.85,1,USD,,511.36,,2.68,0.26,1.55,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1559098598347829,CM2000162911,visa,2021-01-05 16:18:43,CET,Settled,7559098599235848,USD,,81.85,1,USD,,81.11,,0.43,0.06,0.25,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098601176056,CM7000054300,cartebancaire,2021-01-05 16:23:48,CET,Settled,2319098601657122,USD,,498.9,1,USD,,494.9,,2.99,0.01,1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098602170803,CM7000054301,cartebancaire,2021-01-05 16:25:44,CET,Settled,7419098602620564,USD,,498.9,1,USD,,494.9,,2.99,0.01,1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4546098602926519,CM3000151428,mc,2021-01-05 16:26:17,CET,Settled,7549098603771203,USD,,545.85,1,USD,,540.94,,2.84,0.43,1.64,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319098608380340,CM2000162914,cartebancaire,2021-01-05 16:36:08,CET,Settled,2319098609032963,USD,,498.85,1,USD,,494.85,,2.99,0.01,1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6119098607954245,CM7000054305,cartebancaire,2021-01-05 16:36:08,CET,Settled,7419098609041681,USD,,568.8,1,USD,,563.68,,3.41,,1.71,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4846098611011177,CM3000151434,mc,2021-01-05 16:39:03,CET,Settled,7649098611430437,USD,,545.85,1,USD,,540.94,,2.84,0.43,1.64,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6159098611216053,CM2000162917,visa,2021-01-05 16:39:34,CET,Settled,7559098611741815,USD,,944.95,1,USD,,937.87,,4.91,0.28,1.89,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4746098616887887,CM20000002452,visa,2021-01-05 16:48:34,CET,Settled,7649098617149445,USD,,137.8,1,USD,,136.59,,0.72,0.08,0.41,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4316098616561235,CM1000028874,ideal,2021-01-05 16:48:56,CET,Settled,4316098616561235,USD,,585.3,1,USD,,585.05,0.25,,,,idealrabobank,380,,,,,,,,CM1000028874,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1315962210023024,CM700045716,cartebancaire,2021-01-05 16:50:11,CET,Refunded,1316098617350832,USD,461.5,,1,USD,461.5,,,,,,cartebancaire,380,,,,,,,,CM700045716,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1656098619840762,CM7000054306,mc,2021-01-05 16:54:03,CET,Settled,7659098620437669,USD,,508.8,1,USD,,504.73,,2.65,0.4,1.02,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098621031858,CM7000054307,cartebancaire,2021-01-05 16:57:14,CET,Settled,2416098621638476,USD,,518.85,1,USD,,514.69,,3.11,0.01,1.04,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1836098626077492,CM2000162919,visadankort,2021-01-05 17:04:12,CET,Settled,7436098626521458,USD,,479.95,1,USD,,476.34,,2.5,0.15,0.96,visadankort,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1616098632664453,CM1000028875,ideal,2021-01-05 17:15:04,CET,Settled,1616098632664453,USD,,26.45,1,USD,,26.2,0.25,,,,idealing,380,,,,,,,,CM1000028875,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098634301083,CM7000054308,cartebancaire,2021-01-05 17:19:50,CET,Settled,7419098635052389,USD,,423.9,1,USD,,420.5,,2.54,0.01,0.85,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1549098641389649,CM2000162922,mc,2021-01-05 17:29:43,CET,Settled,7549098641835796,USD,,552.85,1,USD,,547.89,,2.87,0.43,1.66,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1839098643550890,CM20000002453,mc,2021-01-05 17:33:13,CET,Settled,7439098643930630,USD,,120.8,1,USD,,119.87,,0.63,0.06,0.24,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6119098644839953,CM3000151444,directEbanking,2021-01-05 17:36:39,CET,Settled,6119098644839953,USD,,50.85,1,USD,,50.34,0.51,,,,directEbanking,380,,,,,,,,CM3000151444,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4615982720540185,CM300134351,amex,2021-01-05 17:40:58,CET,Refunded,1816098648122627,USD,76.5,,1,USD,76.5,,,,,,amex,380,,,,,,,,CM300134351,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4646098645930352,CM2000162924,maestro,2021-01-05 17:41:15,CET,Settled,7649098648754767,USD,,569.8,1,USD,,565.57,,2.96,0.13,1.14,maestro,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1529098653630884,CM7000054309,mc,2021-01-05 17:50:23,CET,Settled,7429098654237844,USD,,568.8,1,USD,,564.25,,2.96,0.45,1.14,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098657908961,CM3000151449,directEbanking,2021-01-05 17:59:16,CET,Settled,4616098657908961,USD,,495.85,1,USD,,490.89,4.96,,,,directEbanking,380,,,,,,,,CM3000151449,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316037899193429,CM300143509,directEbanking,2021-01-05 17:59:56,CET,Refunded,1816098659464421,USD,40,,1,USD,40.2,,0.2,,,,directEbanking,380,,,,,,,,CM300143509,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4316098660263282,CM1000028876,ideal,2021-01-05 18:00:55,CET,Settled,4316098660263282,USD,,56.45,1,USD,,56.2,0.25,,,,idealabn,380,,,,,,,,CM1000028876,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098662718979,CM7000054310,cartebancaire,2021-01-05 18:06:44,CET,Settled,7419098663303530,USD,,82.85,1,USD,,82.09,,0.5,0.01,0.25,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4526098663485664,CM15000004740,mc,2021-01-05 18:07:38,CET,Settled,7429098664581298,USD,,480.95,1,USD,,477.11,,2.5,0.38,0.96,mcpremiumdebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098664619084,CM7000054312,cartebancaire,2021-01-05 18:09:39,CET,Settled,2419098665064800,USD,,511.9,1,USD,,507.29,,3.07,,1.54,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816057939971662,CM100027779,ideal,2021-01-05 18:16:35,CET,Refunded,1816098669505999,USD,419,,1,USD,419.2,,0.2,,,,idealing,380,,,,,,,,CM100027779,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098671105077,CM3000151454,directEbanking,2021-01-05 18:20:15,CET,Settled,1816098671105077,USD,,26.9,1,USD,,26.63,0.27,,,,directEbanking,380,,,,,,,,CM3000151454,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4336098673118875,CM14000007554,visa,2021-01-05 18:22:04,CET,Settled,7936098673241641,USD,,30.9,1,USD,,30.65,,0.16,0.03,0.06,electron,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1346098678442712,CM2000162927,mc,2021-01-05 18:30:51,CET,Settled,7549098678514694,USD,,26.45,1,USD,,26.19,,0.14,0.07,0.05,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098683770023,CM1000028879,ideal,2021-01-05 18:40:34,CET,Settled,1719098683770023,USD,,555.35,1,USD,,555.1,0.25,,,,idealing,380,,,,,,,,CM1000028879,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1536098687703419,CM2000162928,bcmc_mobile,2021-01-05 18:46:16,CET,Settled,7436098687766513,USD,,46.45,1,USD,,46.1,,0.28,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716098688894825,CM7000054314,cartebancaire,2021-01-05 18:49:46,CET,Settled,5316098689026329,USD,,41.55,1,USD,,40.92,,0.25,0.01,0.37,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1326098689284388,CM15000004743,mc,2021-01-05 18:49:58,CET,Settled,7429098689984352,USD,,590.75,1,USD,,586.04,,3.07,0.46,1.18,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098689923686,CM2000162929,ideal,2021-01-05 18:50:42,CET,Settled,1716098689923686,USD,,26.45,1,USD,,26.2,0.25,,,,idealasn,380,,,,,,,,CM2000162929,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1729098690327724,CM3000151457,mc,2021-01-05 18:50:48,CET,Settled,7429098690481562,USD,,46.85,1,USD,,46.4,,0.24,0.07,0.14,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098693348945,CM7000054315,cartebancaire,2021-01-05 18:57:03,CET,Settled,7419098693472693,USD,,49.9,1,USD,,49.49,,0.3,0.01,0.1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4646098695089323,CM2000162931,visa,2021-01-05 18:58:45,CET,Settled,7649098695253291,USD,,120.85,1,USD,,119.79,,0.63,0.07,0.36,visapremiumcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4346098695635193,CM7000054316,mc,2021-01-05 18:59:28,CET,Settled,7649098695683588,USD,,39.9,1,USD,,39.55,,0.21,0.06,0.08,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1759098701345718,CM7000054318,bcmc_mobile,2021-01-05 19:09:01,CET,Settled,7759098701412051,USD,,76.4,1,USD,,75.87,,0.46,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098702846445,CM1000028880,ideal,2021-01-05 19:12:30,CET,Settled,1516098702846445,USD,,26.5,1,USD,,26.25,0.25,,,,idealing,380,,,,,,,,CM1000028880,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098706070300,CM7000054319,cartebancaire,2021-01-05 19:19:07,CET,Settled,5416098706208941,USD,,44.95,1,USD,,44.58,,0.27,0.01,0.09,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098711894517,CM2000162933,cartebancaire,2021-01-05 19:28:37,CET,Settled,2319098712384350,USD,,513.9,1,USD,,509.27,,3.08,0.01,1.54,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1349098713988957,CM3000151465,visa,2021-01-05 19:30:33,CET,Settled,7649098714337109,USD,,495.85,1,USD,,491.53,,2.58,0.25,1.49,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098724985007,CM7000054321,cartebancaire,2021-01-05 19:49:26,CET,Settled,7416098725000549,USD,,49.85,1,USD,,49.44,,0.3,0.01,0.1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1626098730490973,CM2000162936,visadankort,2021-01-05 19:57:44,CET,Settled,7426098730644701,USD,,25.95,1,USD,,25.75,,0.13,0.02,0.05,visadankort,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1759098739207500,CM7000054323,visa,2021-01-05 20:12:07,CET,Settled,7759098739276361,USD,,49.9,1,USD,,49.51,,0.26,0.03,0.1,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098746917404,CM1000028882,ideal,2021-01-05 20:27:44,CET,Settled,4516098746917404,USD,,106.3,1,USD,,106.05,0.25,,,,idealrabobank,380,,,,,,,,CM1000028882,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816098754982347,CM2000162937,directEbanking,2021-01-05 20:40:50,CET,Settled,4816098754982347,USD,,589.75,1,USD,,583.85,5.9,,,,directEbanking,380,,,,,,,,CM2000162937,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1639098765132529,CM3000151476,mc,2021-01-05 20:55:25,CET,Settled,7439098765255709,USD,,41.95,1,USD,,41.54,,0.22,0.06,0.13,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4826098775960486,CM7000054324,mc,2021-01-05 21:14:10,CET,Settled,7926098776500820,USD,,548.8,1,USD,,544.42,,2.85,0.43,1.1,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098776825980,CM1000028883,ideal,2021-01-05 21:15:31,CET,Settled,4516098776825980,USD,,46.45,1,USD,,46.2,0.25,,,,idealing,380,,,,,,,,CM1000028883,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098777850542,CM1000028885,ideal,2021-01-05 21:16:58,CET,Settled,1619098777850542,USD,,475.5,1,USD,,475.25,0.25,,,,idealtriodos,380,,,,,,,,CM1000028885,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098777260506,CM1000028884,ideal,2021-01-05 21:17:01,CET,Settled,1516098777260506,USD,,495.45,1,USD,,495.2,0.25,,,,idealrabobank,380,,,,,,,,CM1000028884,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1819098778201990,CM7000054326,cartebancaire,2021-01-05 21:18:59,CET,Settled,7416098778786085,USD,,99.9,1,USD,,99.09,,0.6,0.01,0.2,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098778907329,CM2000162939,giropay,2021-01-05 21:19:25,CET,Settled,1516098778907329,USD,,545.85,1,USD,,538.55,7.3,,,,giropay,380,,,,,,,,CM2000162939,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098782535640,CM1000028887,ideal,2021-01-05 21:24:58,CET,Settled,1719098782535640,USD,,475.5,1,USD,,475.25,0.25,,,,idealrabobank,380,,,,,,,,CM1000028887,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4736098783434390,CM1000028886,bcmc_mobile,2021-01-05 21:25:58,CET,Settled,7936098783588667,USD,,26.45,1,USD,,26.22,,0.16,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098785683537,CM3000151480,directEbanking,2021-01-05 21:30:25,CET,Settled,4516098785683537,USD,,515.9,1,USD,,510.74,5.16,,,,directEbanking,380,,,,,,,,CM3000151480,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4856098786324808,CM7000054327,visa,2021-01-05 21:31:23,CET,Settled,7759098786834369,USD,,24.95,1,USD,,24.75,,0.13,0.02,0.05,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1859098787109292,CM1000028888,bcmc_mobile,2021-01-05 21:32:08,CET,Settled,7759098787289522,USD,,515.45,1,USD,,512.29,,3.09,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1839098789585718,CM7000054329,mc,2021-01-05 21:37:01,CET,Settled,7436098790217453,USD,,545.4,1,USD,,540.49,,2.84,0.43,1.64,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4326098790016921,CM20000002454,mc,2021-01-05 21:37:01,CET,Settled,7426098790215798,USD,,571.85,1,USD,,560.85,,2.97,1.45,6.58,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098792435313,CM3000151481,directEbanking,2021-01-05 21:42:07,CET,Settled,1716098792435313,USD,,46.9,1,USD,,46.43,0.47,,,,directEbanking,380,,,,,,,,CM3000151481,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098795263410,CM3000151483,directEbanking,2021-01-05 21:46:44,CET,Settled,4616098795263410,USD,,76.85,1,USD,,76.08,0.77,,,,directEbanking,380,,,,,,,,CM3000151483,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098795574641,CM1000028889,ideal,2021-01-05 21:47:20,CET,Settled,1316098795574641,USD,,46.45,1,USD,,46.2,0.25,,,,idealrabobank,380,,,,,,,,CM1000028889,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1656098797179298,CM1000028890,visa,2021-01-05 21:52:25,CET,Settled,7659098799454966,USD,,430.95,1,USD,,427.2,,2.24,0.22,1.29,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319098800205881,CM2000162940,ideal,2021-01-05 21:54:35,CET,Settled,1319098800205881,USD,,465.4,1,USD,,465.15,0.25,,,,idealrabobank,380,,,,,,,,CM2000162940,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098801919765,CM1000028891,ideal,2021-01-05 22:00:31,CET,Settled,4616098801919765,USD,,51.5,1,USD,,51.25,0.25,,,,idealrabobank,380,,,,,,,,CM1000028891,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098803860017,CM7000054331,cartebancaire,2021-01-05 22:02:17,CET,Settled,7416098804619375,USD,,292.65,1,USD,,290.3,,1.76,,0.59,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1559098804925731,CM2000162941,mc,2021-01-05 22:02:37,CET,Settled,7759098805570275,USD,,549.85,1,USD,,543.95,,2.86,1.39,1.65,mcsuperpremiumcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6119098806188504,CM2000162942,ideal,2021-01-05 22:04:04,CET,Settled,6119098806188504,USD,,46.45,1,USD,,46.2,0.25,,,,idealing,380,,,,,,,,CM2000162942,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4756098806465830,CM2000162943,mc,2021-01-05 22:04:52,CET,Settled,7759098806920040,USD,,565.8,1,USD,,560.72,,2.94,0.44,1.7,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6129098808836025,CM7000054332,visa,2021-01-05 22:08:11,CET,Settled,7426098808917344,USD,,39.9,1,USD,,39.53,,0.21,0.04,0.12,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1856098811821154,CM1000028893,bcmc_mobile,2021-01-05 22:13:17,CET,Settled,7559098811975692,USD,,106.35,1,USD,,105.64,,0.64,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816098813893485,CM7000054333,cartebancaire,2021-01-05 22:17:44,CET,Settled,7916098813938027,USD,,34.95,1,USD,,34.66,,0.21,0.01,0.07,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816098814282134,CM3000151485,directEbanking,2021-01-05 22:18:11,CET,Settled,4816098814282134,USD,,21.95,1,USD,,21.73,0.22,,,,directEbanking,380,,,,,,,,CM3000151485,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1819098817721438,CM1000028894,ideal,2021-01-05 22:23:31,CET,Settled,1819098817721438,USD,,46.4,1,USD,,46.15,0.25,,,,idealabn,380,,,,,,,,CM1000028894,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098818309976,CM1000028895,ideal,2021-01-05 22:24:18,CET,Settled,4616098818309976,USD,,475.5,1,USD,,475.25,0.25,,,,idealrabobank,380,,,,,,,,CM1000028895,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4536098821511581,CM17000002643,visa,2021-01-05 22:29:54,CET,Settled,7439098821946028,USD,,54.83,1,USD,,53.5,,0.29,0.22,0.82,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4336098821908037,CM3000151488,visa,2021-01-05 22:30:26,CET,Settled,7439098822262839,USD,,515.9,1,USD,,508.59,,2.68,0.16,4.47,visacommercialdebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1556098823411971,CM3000151489,mc,2021-01-05 22:32:32,CET,Settled,7559098823527952,USD,,30.9,1,USD,,30.64,,0.16,0.04,0.06,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098823386675,CM7000054335,cartebancaire,2021-01-05 22:34:43,CET,Settled,7916098824278629,USD,,79.85,1,USD,,79.2,,0.48,0.01,0.16,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098825320671,CM1000028896,ideal,2021-01-05 22:35:55,CET,Settled,1816098825320671,USD,,86.4,1,USD,,86.15,0.25,,,,idealbunq,380,,,,,,,,CM1000028896,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619097571396090,CM1000028824,bankTransfer_BE,2021-01-05 22:36:22,CET,Settled,1619097571396090,USD,,475.5,1,USD,,475.3,0.2,,,,bankTransfer_BE,380,,,,,,,,CM1000028824,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816098610331689,CM2000161980,bankTransfer_BE,2021-01-05 22:36:24,CET,Settled,4816098610331689,USD,,508.45,1,USD,,508.25,0.2,,,,bankTransfer_BE,380,,,,,,,,CM2000161980,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098825789542,CM7000054337,cartebancaire,2021-01-05 22:38:10,CET,Settled,7916098826286190,USD,,122.75,1,USD,,121.76,,0.74,,0.25,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4546098829397452,CM2000162949,visadankort,2021-01-05 22:43:05,CET,Settled,7549098829859485,USD,,195.8,1,USD,,194.32,,1.02,0.07,0.39,visadankort,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1616096929077843,CM3000151232,bankTransfer_DE,2021-01-05 22:46:23,CET,Settled,1616096929077843,USD,,96.9,1,USD,,96.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151232,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716097806581567,CM3000151330,bankTransfer_DE,2021-01-05 22:46:25,CET,Settled,1716097806581567,USD,,136.9,1,USD,,136.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151330,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616093212265072,CM3000150712,bankTransfer_DE,2021-01-05 22:46:27,CET,Settled,4616093212265072,USD,,495.9,1,USD,,495.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150712,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716094450448429,CM2000162667,bankTransfer_DE,2021-01-05 22:46:27,CET,Settled,4716094450448429,USD,,475.95,1,USD,,475.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162667,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716092564273816,CM3000150631,bankTransfer_DE,2021-01-05 22:46:27,CET,Settled,4716092564273816,USD,,505.9,1,USD,,505.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150631,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719095902053041,CM3000151134,bankTransfer_DE,2021-01-05 22:46:29,CET,Settled,1719095902053041,USD,,21.95,1,USD,,21.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151134,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619097829807427,CM3000151335,bankTransfer_DE,2021-01-05 22:46:35,CET,Settled,1619097829807427,USD,,71.95,1,USD,,71.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151335,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519094275551766,CM3000150968,bankTransfer_DE,2021-01-05 22:46:36,CET,Settled,1519094275551766,USD,,475.95,1,USD,,475.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150968,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719094299491781,CM3000150979,bankTransfer_DE,2021-01-05 22:46:42,CET,Settled,1719094299491781,USD,,66.95,1,USD,,66.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150979,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716096664441385,CM3000151192,bankTransfer_DE,2021-01-05 22:46:59,CET,Settled,1716096664441385,USD,,26.95,1,USD,,26.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151192,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716097985927094,CM3000151359,bankTransfer_DE,2021-01-05 22:47:01,CET,Settled,1716097985927094,USD,,46.85,1,USD,,46.65,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151359,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219093569570674,CM3000150828,bankTransfer_DE,2021-01-05 22:47:07,CET,Settled,6219093569570674,USD,,91.95,1,USD,,91.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150828,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619093490920383,CM3000150799,bankTransfer_DE,2021-01-05 22:47:17,CET,Settled,1619093490920383,USD,,465.9,1,USD,,465.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150799,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716095921917579,CM2000162731,bankTransfer_DE,2021-01-05 22:47:17,CET,Settled,4716095921917579,USD,,515.9,1,USD,,515.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162731,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316093661120967,CM2000162609,bankTransfer_DE,2021-01-05 22:47:17,CET,Settled,1316093661120967,USD,,515.9,1,USD,,515.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162609,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619094172177714,CM3000150919,bankTransfer_DE,2021-01-05 22:47:17,CET,Settled,1619094172177714,USD,,515.9,1,USD,,515.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150919,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816091036379804,CM3000150388,bankTransfer_DE,2021-01-05 22:47:18,CET,Settled,4816091036379804,USD,,545.85,1,USD,,545.65,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150388,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316095879575577,CM3000151130,bankTransfer_DE,2021-01-05 22:47:18,CET,Settled,1316095879575577,USD,,545.85,1,USD,,545.65,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151130,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098212250449,CM3000151364,bankTransfer_DE,2021-01-05 22:47:20,CET,Settled,1619098212250449,USD,,26.9,1,USD,,26.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151364,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719082373216828,CM3000149556,bankTransfer_DE,2021-01-05 22:47:24,CET,Settled,1719082373216828,USD,,568.8,1,USD,,568.6,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000149556,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098329962825,CM2000162872,bankTransfer_DE,2021-01-05 22:47:28,CET,Settled,1316098329962825,USD,,475.95,1,USD,,475.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162872,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098366040068,CM3000151379,bankTransfer_DE,2021-01-05 22:47:29,CET,Settled,1519098366040068,USD,,61.95,1,USD,,61.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151379,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319097730887677,CM2000162825,bankTransfer_DE,2021-01-05 22:47:30,CET,Settled,1319097730887677,USD,,475.95,1,USD,,475.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162825,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716096125751471,CM3000151166,bankTransfer_DE,2021-01-05 22:47:31,CET,Settled,1716096125751471,USD,,56.85,1,USD,,56.65,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151166,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6119097984495277,CM2000162849,bankTransfer_DE,2021-01-05 22:47:33,CET,Settled,6119097984495277,USD,,535.85,1,USD,,535.65,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162849,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098833255984,CM7000054339,cartebancaire,2021-01-05 22:50:00,CET,Settled,2319098833417964,USD,,49.85,1,USD,,49.44,,0.3,0.01,0.1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098837274028,CM7000054340,cartebancaire,2021-01-05 22:57:54,CET,Settled,7416098837989761,USD,,548.85,1,USD,,544.45,,3.29,0.01,1.1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098838185283,CM7000054341,cartebancaire,2021-01-05 22:58:55,CET,Settled,2319098838335674,USD,,49.9,1,USD,,49.49,,0.3,0.01,0.1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319098839578639,CM1000028898,ideal,2021-01-05 22:59:38,CET,Settled,1319098839578639,USD,,21.5,1,USD,,21.25,0.25,,,,idealing,380,,,,,,,,CM1000028898,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098840081330,CM7000054342,cartebancaire,2021-01-05 23:02:06,CET,Settled,7419098840236918,USD,,29.9,1,USD,,29.65,,0.18,0.01,0.06,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319098843583005,CM7000054343,cartebancaire,2021-01-05 23:08:26,CET,Settled,2316098844388595,USD,,79.85,1,USD,,79.12,,0.48,0.01,0.24,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1729098847315127,CM2000162952,mc,2021-01-05 23:13:50,CET,Settled,7426098848302184,USD,,469.9,1,USD,,465.7,,2.44,0.82,0.94,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716098852529991,CM3000151494,directEbanking,2021-01-05 23:22:58,CET,Settled,4716098852529991,USD,,445.9,1,USD,,441.44,4.46,,,,directEbanking,380,,,,,,,,CM3000151494,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2021-01-07 00:46:38,CET,Fee,Transaction Fees January 05 2021,,,,,USD,38.1,,,,,,,380,,,,,,,,,2021-01-07 00:46:38,CET +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2021-01-07 00:46:38,CET,MerchantPayout,"TX635094301XT batch 380, YOURCOMPANY_ACCOUNT",,,,,USD,55308.07,,,,,,,380,,,,,,,,,2021-01-07 00:46:38,CET 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 ca75591a9..66488ba2d 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -50,11 +50,28 @@ def test_02_import_adyen_credit_fees(self): def test_03_import_adyen_invalid(self): """ Trying to hit that coverall target """ - with self.assertRaisesRegex(UserError, "Could not make sense"): + with self.assertRaisesRegex(UserError, "not a Adyen settlement details file"): self._test_statement_import( "adyen_test_invalid.xls", "invalid", ) + def test_04_import_adyen_csv(self): + """ Test that the Adyen statement can be imported in csv format.""" + self._test_statement_import( + "settlement_detail_report_batch_380.csv", "YOURCOMPANY_ACCOUNT 2021/380", + ) + statement = self.env["account.bank.statement"].search( + [], order="create_date desc", limit=1 + ) + self.assertEqual(statement.journal_id, self.journal) + # Csv lines has 229 lines. Minus 1 header. Plus 1 extra transaction line. + self.assertEqual(len(statement.line_ids), 229) + self.assertTrue( + self.env.user.company_id.currency_id.is_zero( + sum(line.amount for line in statement.line_ids) + ) + ) + def _test_statement_import(self, file_name, statement_name): """Test correct creation of single statement.""" testfile = get_module_resource( @@ -63,12 +80,12 @@ def _test_statement_import(self, file_name, statement_name): with open(testfile, "rb") as datafile: data_file = base64.b64encode(datafile.read()) import_wizard = self.env["account.bank.statement.import"].create( - {"attachment_ids": [(0, 0, {"name": "test file", "datas": data_file})]} + {"attachment_ids": [(0, 0, {"name": file_name, "datas": data_file})]} ) import_wizard.with_context( {"account_bank_statement_import_adyen": True} ).import_file() - # statement name is account number + '-' + date of last line: + # statement name is account number + '-' + date of last line. statements = self.env["account.bank.statement"].search( [("name", "=", statement_name)] ) diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index 62a9babd2..f938d7f14 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -38,16 +38,11 @@ def _pull(self, date_since, date_until): # noqa: C901 date_since, date_until ) for provider in adyen_providers: - # TODO: incrementing batch number is_scheduled = self.env.context.get("scheduled") try: - data_file = self._adyen_get_settlement_details_file() + data_file, filename = self._adyen_get_settlement_details_file() import_wizard = self.env["account.bank.statement.import"].create( - { - "attachment_ids": [ - (0, 0, {"name": "test file", "datas": data_file}) - ] - } + {"attachment_ids": [(0, 0, {"name": filename, "datas": data_file})]} ) import_wizard.with_context( {"account_bank_statement_import_adyen": True} @@ -84,13 +79,13 @@ def _adyen_get_settlement_details_file(self): [YourMerchantAccount]/[ReportFileName]" """ batch_number = self.next_batch_number - download_file_name = self.download_file_name % batch_number + filename = self.download_file_name % batch_number URL = "/".join( - [self.api_base, self.journal_id.adyen_merchant_account, download_file_name] + [self.api_base, self.journal_id.adyen_merchant_account, filename] ) response = requests.get(URL, auth=(self.username, self.password)) if response.status_code == 200: - return response.content + return response.content, filename else: raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) diff --git a/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py index 9c42c0aab..39adbe372 100644 --- a/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py +++ b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py @@ -14,10 +14,11 @@ def _adyen_get_settlement_details_file(self): if self.service != "dummy_adyen": # Not a dummy, get the regular adyen method. return super()._adyen_get_settlement_details_file() + filename = self.download_file_name testfile = get_module_resource( - "account_bank_statement_import_adyen", "test_files", self.download_file_name + "account_bank_statement_import_adyen", "test_files", filename ) with open(testfile, "rb") as datafile: data_file = datafile.read() data_file = base64.b64encode(data_file) - return data_file + return data_file, filename diff --git a/account_bank_statement_import_online_adyen/tests/test_import_online.py b/account_bank_statement_import_online_adyen/tests/test_import_online.py index 71c0d7653..9921e51f2 100644 --- a/account_bank_statement_import_online_adyen/tests/test_import_online.py +++ b/account_bank_statement_import_online_adyen/tests/test_import_online.py @@ -54,7 +54,7 @@ def _test_statement_import(self, file_name, statement_name): # Pull from yesterday, until today yesterday = self.now - relativedelta(days=1) provider.with_context(scheduled=True)._pull(yesterday, self.now) - # statement name is account number + '-' + date of last line: + # statement name is account number + '-' + date of last line. statements = self.env["account.bank.statement"].search( [("name", "=", statement_name)] ) From 9dc19537d2b755799f8116746acca477ad92cc48 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 16 Dec 2021 15:49:24 +0100 Subject: [PATCH 12/24] [FIX] *_online_adyen: correct icon --- .../static/description/icon.png | Bin 11070 -> 2720 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/account_bank_statement_import_online_adyen/static/description/icon.png b/account_bank_statement_import_online_adyen/static/description/icon.png index 09847ed7696bcad441aac277011add9a9a82c01d..716dbd37cca93ec73ce2f21cad007f2ee8ab9ed3 100644 GIT binary patch literal 2720 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 From 0b91fce1d40fe62a79df2ea5edb312d19877c8f1 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 16 Dec 2021 17:29:45 +0100 Subject: [PATCH 13/24] [FIX] *_online_adyen: make sure downloaded file contains valid base64 encode bytes --- .../models/online_bank_statement_provider.py | 14 +++++++++++--- .../account_bank_statement_import_online_adyen | 1 + .../setup.py | 6 ++++++ 3 files changed, 18 insertions(+), 3 deletions(-) create mode 120000 setup/account_bank_statement_import_online_adyen/odoo/addons/account_bank_statement_import_online_adyen create mode 100644 setup/account_bank_statement_import_online_adyen/setup.py diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index f938d7f14..5d982f165 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -84,10 +84,18 @@ def _adyen_get_settlement_details_file(self): [self.api_base, self.journal_id.adyen_merchant_account, filename] ) response = requests.get(URL, auth=(self.username, self.password)) - if response.status_code == 200: - return response.content, filename - else: + if response.status_code != 200: raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) + # Check base64 decoding and padding of response.content. + # Remember: response.text is unicode, response.content is in bytes. + byte_count = len(response.content) + _logger.debug( + _("Retrieved %d bytes, starting with %s"), byte_count, response.text[:32] + ) + # Make sure base64 encoded content contains multiple of 4 bytes. + byte_padding = b"=" * (byte_count % 4) + data_file = response.content + byte_padding + return data_file, filename def _schedule_next_run(self): """Set next run date and autoincrement batch number.""" diff --git a/setup/account_bank_statement_import_online_adyen/odoo/addons/account_bank_statement_import_online_adyen b/setup/account_bank_statement_import_online_adyen/odoo/addons/account_bank_statement_import_online_adyen new file mode 120000 index 000000000..fa4466014 --- /dev/null +++ b/setup/account_bank_statement_import_online_adyen/odoo/addons/account_bank_statement_import_online_adyen @@ -0,0 +1 @@ +../../../../account_bank_statement_import_online_adyen \ No newline at end of file diff --git a/setup/account_bank_statement_import_online_adyen/setup.py b/setup/account_bank_statement_import_online_adyen/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/account_bank_statement_import_online_adyen/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 169d704d21803aa0567822cff52ce632d577941b Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Fri, 17 Dec 2021 10:08:36 +0100 Subject: [PATCH 14/24] [IMP] *_online_adyen: more debugging on retrieved statement --- .../models/online_bank_statement_provider.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index 5d982f165..99f73aa9b 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -88,9 +88,17 @@ def _adyen_get_settlement_details_file(self): raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) # Check base64 decoding and padding of response.content. # Remember: response.text is unicode, response.content is in bytes. + text_count = len(response.text) + _logger.debug( + _("Retrieved %d length text from Adyen, starting with %s"), + text_count, + response.text[:64], + ) byte_count = len(response.content) _logger.debug( - _("Retrieved %d bytes, starting with %s"), byte_count, response.text[:32] + _("Retrieved %d bytes from Adyen, starting with %s"), + byte_count, + response.content[:64], ) # Make sure base64 encoded content contains multiple of 4 bytes. byte_padding = b"=" * (byte_count % 4) From a41e96ae9a9f93b2c27656596db3eefc037ed32d Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Fri, 17 Dec 2021 12:17:46 +0100 Subject: [PATCH 15/24] [IMP] *_online_adyen base64 encode retrieved file --- .../models/online_bank_statement_provider.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index 99f73aa9b..04fbb1cc4 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -1,5 +1,6 @@ # Copyright 2021 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 import logging from html import escape @@ -40,9 +41,9 @@ def _pull(self, date_since, date_until): # noqa: C901 for provider in adyen_providers: is_scheduled = self.env.context.get("scheduled") try: - data_file, filename = self._adyen_get_settlement_details_file() + attachment_vals = self._get_attachment_vals() import_wizard = self.env["account.bank.statement.import"].create( - {"attachment_ids": [(0, 0, {"name": filename, "datas": data_file})]} + {"attachment_ids": [(0, 0, attachment_vals)]} ) import_wizard.with_context( {"account_bank_statement_import_adyen": True} @@ -67,6 +68,17 @@ def _pull(self, date_since, date_until): # noqa: C901 if is_scheduled: provider._schedule_next_run() + def _get_attachment_vals(self): + """Retrieve settlement details and convert to attachment vals.""" + content, filename = self._adyen_get_settlement_details_file() + encoded_content = base64.encodebytes(content) + # Make sure base64 encoded content contains multiple of 4 bytes. + byte_count = len(encoded_content) + byte_padding = b"=" * (byte_count % 4) + data_file = encoded_content + byte_padding + attachment_vals = {"name": filename, "datas": data_file} + return attachment_vals + def _adyen_get_settlement_details_file(self): """Retrieve daily generated settlement details file. @@ -86,6 +98,7 @@ def _adyen_get_settlement_details_file(self): response = requests.get(URL, auth=(self.username, self.password)) if response.status_code != 200: raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) + _logger.debug(_("Headers returned by Adyen %s"), response.headers) # Check base64 decoding and padding of response.content. # Remember: response.text is unicode, response.content is in bytes. text_count = len(response.text) @@ -100,10 +113,7 @@ def _adyen_get_settlement_details_file(self): byte_count, response.content[:64], ) - # Make sure base64 encoded content contains multiple of 4 bytes. - byte_padding = b"=" * (byte_count % 4) - data_file = response.content + byte_padding - return data_file, filename + return response.content, filename def _schedule_next_run(self): """Set next run date and autoincrement batch number.""" From f16b1e72f74f1d4f8f382f55c5c21185e9f0f0fe Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Fri, 25 Feb 2022 09:31:44 +0100 Subject: [PATCH 16/24] [FIX] *adyen*: support xlsx as well as csv --- .../models/account_bank_statement_import.py | 52 +++++++++++++------ .../models/online_bank_statement_provider.py | 12 ++--- 2 files changed, 38 insertions(+), 26 deletions(-) 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 649d882ab..64a4870e0 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,12 +1,14 @@ # Copyright 2017 Opener BV () -# Copyright 2021 Therp BV . +# Copyright 2021-2022 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +"""Add import of Adyen statements.""" +# pylint: disable=protected-access,no-self-use import logging from odoo import _, fields, models from odoo.exceptions import UserError -_logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) # pylint: disable=invalid-name COLUMNS = { "Company Account": 1, @@ -43,6 +45,8 @@ class AccountBankStatementImport(models.TransientModel): + """Add import of Adyen statements.""" + _inherit = "account.bank.statement.import" def _parse_file(self, data_file): @@ -50,7 +54,7 @@ def _parse_file(self, data_file): try: _logger.debug(_("Try parsing as Adyen settlement details.")) return self._parse_adyen_file(data_file) - except Exception: + except Exception: # pylint: disable=broad-except message = _("Statement file was not a Adyen settlement details file.") if self.env.context.get("account_bank_statement_import_adyen", False): raise UserError(message) @@ -58,7 +62,7 @@ def _parse_file(self, data_file): return super()._parse_file(data_file) def _find_additional_data(self, currency_code, account_number): - """Try to find journal by Adyen merchant account.""" + """Check if journal passed in the context matches Adyen Merchant Account.""" if account_number: journal = self.env["account.journal"].search( [("adyen_merchant_account", "=", account_number)], limit=1 @@ -73,33 +77,39 @@ def _find_additional_data(self, currency_code, account_number): ) % account_number ) - self = self.with_context(journal_id=journal.id) return super()._find_additional_data(currency_code, account_number) def _parse_adyen_file(self, data_file): """Parse file assuming it is an Adyen file. - An Excception will be thrown if file cannot be parsed. + An Exception will be thrown if file cannot be parsed. """ statement = None headers = False + batch_number = False fees = 0.0 balance = 0.0 payout = 0.0 rows = self._get_rows(data_file) + num_rows = 0 for row in rows: - if len(row) < 24: - raise ValueError( - "Not an Adyen statement. Unexpected row length %s " - "less then minimum of 24" % len(row) - ) + num_rows += 1 if not row[1]: continue if not headers: + on_header_row = self._check_header_row(row) + if not on_header_row: + continue self._set_columns(row) headers = True continue + if len(row) < 24: + raise ValueError( + "Not an Adyen statement. Unexpected row length %s " + "less then minimum of 24" % len(row) + ) if not statement: + batch_number = self._get_value(row, "Batch Number") statement = self._make_statement(row) currency_code = self._get_value(row, "Net Currency") merchant_id = self._get_value(row, "Merchant Account") @@ -112,12 +122,14 @@ def _parse_adyen_file(self, data_file): balance += self._balance(row) self._import_adyen_transaction(statement, row) fees += self._sum_fees(row) - if not headers: - raise ValueError("Not an Adyen statement. Did not encounter header row.") + raise ValueError( + "Not an Adyen statement. Did not encounter header row in %d rows." + % (num_rows,) + ) if fees: balance -= fees - self._add_fees_transaction(statement, fees, row) + self._add_fees_transaction(statement, fees, batch_number) 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: @@ -139,6 +151,13 @@ def _get_rows(self, data_file): importer = import_model.create({"file": data_file, "file_name": filename}) return importer._read_file({"quoting": '"', "separator": ","}) + def _check_header_row(self, row): + """Header row is the first one with a "Company Account" header cell.""" + for cell in row: + if cell == "Company Account": + return True + return False + def _set_columns(self, row): """Set columns from headers. There MUST be a 'Company Account' header.""" seen_company_account = False @@ -229,13 +248,12 @@ def _get_unique_import_id(self, statement): """get unique import ID for transaction.""" return statement["name"] + str(len(statement["transactions"])).zfill(4) - def _add_fees_transaction(self, statement, fees, row): + def _add_fees_transaction(self, statement, fees, batch_number): """Single transaction for all fees in statement.""" transaction = dict( unique_import_id=self._get_unique_import_id(statement), date=max(t["date"] for t in statement["transactions"]), amount=-fees, - name="Commission, markup etc. batch %s" - % self._get_value(row, "Batch Number"), + name="Commission, markup etc. batch %s" % batch_number, ) statement["transactions"].append(transaction) diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index 04fbb1cc4..4e6ef44d0 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -1,5 +1,6 @@ # Copyright 2021 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +# pylint: disable=missing-docstring,invalid-name,protected-access import base64 import logging from html import escape @@ -52,7 +53,8 @@ def _pull(self, date_since, date_until): # noqa: C901 if is_scheduled: _logger.warning( 'Online Bank Statement Provider "%s" failed to' - " obtain statement data" % (provider.name,), + " obtain statement data", + provider.name, exc_info=True, ) provider.message_post( @@ -99,14 +101,6 @@ def _adyen_get_settlement_details_file(self): if response.status_code != 200: raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) _logger.debug(_("Headers returned by Adyen %s"), response.headers) - # Check base64 decoding and padding of response.content. - # Remember: response.text is unicode, response.content is in bytes. - text_count = len(response.text) - _logger.debug( - _("Retrieved %d length text from Adyen, starting with %s"), - text_count, - response.text[:64], - ) byte_count = len(response.content) _logger.debug( _("Retrieved %d bytes from Adyen, starting with %s"), From 321c39ee783decfb7d6de5fbbc696d136bb5fec0 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Mon, 7 Mar 2022 13:29:04 +0100 Subject: [PATCH 17/24] [IMP] *import_adyen: log rows read and transaction count --- .../models/account_bank_statement_import.py | 7 +++++++ 1 file changed, 7 insertions(+) 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 64a4870e0..7b1227992 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 @@ -92,6 +92,7 @@ def _parse_adyen_file(self, data_file): payout = 0.0 rows = self._get_rows(data_file) num_rows = 0 + num_transactions = 0 for row in rows: num_rows += 1 if not row[1]: @@ -120,6 +121,7 @@ def _parse_adyen_file(self, data_file): payout -= self._balance(row) else: balance += self._balance(row) + num_transactions += 1 self._import_adyen_transaction(statement, row) fees += self._sum_fees(row) if not headers: @@ -137,6 +139,11 @@ def _parse_adyen_file(self, data_file): _("Parse error. Balance %s not equal to merchant " "payout %s") % (balance, payout) ) + _logger.info( + _("Processed %d rows from Adyen statement file with %d transactions"), + num_rows, + num_transactions, + ) return currency_code, merchant_id, [statement] def _get_rows(self, data_file): From 7eb030f928783e18dd0497e7a433c51a46f992db Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 17 Mar 2022 13:23:54 +0100 Subject: [PATCH 18/24] [IMP] *_import_adyen: Support statements without payout --- .../models/account_bank_statement_import.py | 5 +++- .../settlement_detail_report_batch_238.csv | 4 +++ .../tests/test_import_adyen.py | 27 +++++++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv 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 7b1227992..dffb4439f 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 @@ -84,6 +84,7 @@ def _parse_adyen_file(self, data_file): An Exception will be thrown if file cannot be parsed. """ + # pylint: disable=too-many-locals,too-many-branches statement = None headers = False batch_number = False @@ -133,7 +134,7 @@ def _parse_adyen_file(self, data_file): balance -= fees self._add_fees_transaction(statement, fees, batch_number) if statement["transactions"] and not payout: - raise UserError(_("No payout detected in Adyen statement.")) + _logger.info(_("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") @@ -169,6 +170,8 @@ def _set_columns(self, row): """Set columns from headers. There MUST be a 'Company Account' header.""" seen_company_account = False for num, header in enumerate(row): + if not header.strip(): + continue # Ignore empty columns. if header == "Company Account": seen_company_account = True if header not in COLUMNS: diff --git a/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv new file mode 100644 index 000000000..164262cc3 --- /dev/null +++ b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv @@ -0,0 +1,4 @@ +Company Account,Merchant Account,Psp Reference,Merchant Reference,Payment Method,Creation Date,TimeZone,Type,Modification Reference,Gross Currency,Gross Debit (GC),Gross Credit (GC),Exchange Rate,Net Currency,Net Debit (NC),Net Credit (NC),Commission (NC),Markup (NC),Scheme Fees (NC),Interchange (NC),Payment Method Variant,Modification Merchant Reference,Batch Number,Reserved4,Reserved5,Reserved6,Reserved7,Reserved8,Reserved9,Reserved10 +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-02-22 00:24:23,CET,Balancetransfer,Balancetransfer,,,,,EUR,,454331.99,,,,,,Balancetransfer from batch 237,238,,,,,,, +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Fee,Transaction Fees March 01 2022,,,,,EUR,0.09,,,,,,,,238,,,,,,, +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Balancetransfer,Balancetransfer,,,,,EUR,454331.90,,,,,,,Balancetransfer to batch 239,238,,,,,,, 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 66488ba2d..ed6e57b85 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -1,7 +1,8 @@ # Copyright 2017 Opener BV # Copyright 2020 Vanmoof BV -# Copyright 2015-2021 Therp BV ) +# Copyright 2015-2022 Therp BV ) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +"""Run test imports of Adyen files.""" import base64 from odoo.exceptions import UserError @@ -10,6 +11,8 @@ class TestImportAdyen(SavepointCase): + """Run test imports of Adyen files.""" + @classmethod def setUpClass(cls): super().setUpClass() @@ -72,6 +75,23 @@ def test_04_import_adyen_csv(self): ) ) + def test_05_import_adyen_csv(self): + """ Test that the Adyen statement without Merchant Payoutcan be imported.""" + self._test_statement_import( + "settlement_detail_report_batch_238.csv", "YOURCOMPANY_ACCOUNT 2022/238", + ) + statement = self.env["account.bank.statement"].search( + [], order="create_date desc", limit=1 + ) + self.assertEqual(statement.journal_id, self.journal) + # Csv lines has 4 lines. Minus 1 header. No extra transaction line. + self.assertEqual(len(statement.line_ids), 3) + self.assertTrue( + self.env.user.company_id.currency_id.is_zero( + sum(line.amount for line in statement.line_ids) + ) + ) + def _test_statement_import(self, file_name, statement_name): """Test correct creation of single statement.""" testfile = get_module_resource( @@ -83,7 +103,10 @@ def _test_statement_import(self, file_name, statement_name): {"attachment_ids": [(0, 0, {"name": file_name, "datas": data_file})]} ) import_wizard.with_context( - {"account_bank_statement_import_adyen": True} + { + "account_bank_statement_import_adyen": True, + "journal_id": self.journal.id, + } ).import_file() # statement name is account number + '-' + date of last line. statements = self.env["account.bank.statement"].search( From 499c08e37551d9cf5bf5a50ed378acaa3a1f42a9 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 17 Mar 2022 15:58:25 +0100 Subject: [PATCH 19/24] [IMP] *_online_adyen: Directly parse retrieved adyen file Prevent other statement import modules from processing the file. Without this for instance the Enterprise account_bank_statement_import_csv module will process a retrieved cv file. In addition this is more efficient as it saves a decoding step. --- .../models/online_bank_statement_provider.py | 25 +++++++++++++------ .../online_bank_statement_provider_dummy.py | 8 +++--- .../tests/test_import_online.py | 7 +++++- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index 4e6ef44d0..97d3467d0 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -42,13 +42,7 @@ def _pull(self, date_since, date_until): # noqa: C901 for provider in adyen_providers: is_scheduled = self.env.context.get("scheduled") try: - attachment_vals = self._get_attachment_vals() - import_wizard = self.env["account.bank.statement.import"].create( - {"attachment_ids": [(0, 0, attachment_vals)]} - ) - import_wizard.with_context( - {"account_bank_statement_import_adyen": True} - ).import_file() + self._import_adyen_file() except BaseException as e: if is_scheduled: _logger.warning( @@ -70,6 +64,21 @@ def _pull(self, date_since, date_until): # noqa: C901 if is_scheduled: provider._schedule_next_run() + def _import_adyen_file(self): + """Import Adyen file using functionality from manual Adyen import module.""" + self.ensure_one() + content, attachment_vals = self._get_attachment_vals() + wizard = ( + self.env["account.bank.statement.import"] + .with_context({"journal_id": self.journal_id.id}) + .create({"attachment_ids": [(0, 0, attachment_vals)]}) + ) + currency_code, account_number, stmts_vals = wizard._parse_adyen_file(content) + wizard._check_parsed_data(stmts_vals, account_number) + _currency, journal = wizard._find_additional_data(currency_code, account_number) + stmts_vals = wizard._complete_stmts_vals(stmts_vals, journal, account_number) + wizard._create_bank_statements(stmts_vals) + def _get_attachment_vals(self): """Retrieve settlement details and convert to attachment vals.""" content, filename = self._adyen_get_settlement_details_file() @@ -79,7 +88,7 @@ def _get_attachment_vals(self): byte_padding = b"=" * (byte_count % 4) data_file = encoded_content + byte_padding attachment_vals = {"name": filename, "datas": data_file} - return attachment_vals + return content, attachment_vals def _adyen_get_settlement_details_file(self): """Retrieve daily generated settlement details file. diff --git a/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py index 39adbe372..9b028cb4d 100644 --- a/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py +++ b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py @@ -1,12 +1,13 @@ -# Copyright 2021 Therp BV . +# Copyright 2021-2022 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -import base64 - +"""Dummy provider gets files from file-system, instead of from api.""" from odoo import models from odoo.modules.module import get_module_resource class OnlineBankStatementProviderDummy(models.Model): + """Dummy provider gets files from file-system, instead of from api.""" + _inherit = "online.bank.statement.provider" def _adyen_get_settlement_details_file(self): @@ -20,5 +21,4 @@ def _adyen_get_settlement_details_file(self): ) with open(testfile, "rb") as datafile: data_file = datafile.read() - data_file = base64.b64encode(data_file) return data_file, filename diff --git a/account_bank_statement_import_online_adyen/tests/test_import_online.py b/account_bank_statement_import_online_adyen/tests/test_import_online.py index 9921e51f2..64d42de41 100644 --- a/account_bank_statement_import_online_adyen/tests/test_import_online.py +++ b/account_bank_statement_import_online_adyen/tests/test_import_online.py @@ -1,9 +1,11 @@ -# Copyright 2021 Therp BV . +# Copyright 2021-2022 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +"""Test online Adyen reusing tests for manual import.""" from dateutil.relativedelta import relativedelta from odoo import fields +# pylint: disable=import-error from odoo.addons.account_bank_statement_import_adyen.tests.test_import_adyen import ( TestImportAdyen, ) @@ -14,6 +16,8 @@ class TestImportOnline(TestImportAdyen): @classmethod def setUpClass(cls): + """Setup online journal.""" + # pylint: disable=invalid-name super().setUpClass() cls.now = fields.Datetime.now() cls.journal.write( @@ -53,6 +57,7 @@ def _test_statement_import(self, file_name, statement_name): ) # Pull from yesterday, until today yesterday = self.now - relativedelta(days=1) + # pylint: disable=protected-access provider.with_context(scheduled=True)._pull(yesterday, self.now) # statement name is account number + '-' + date of last line. statements = self.env["account.bank.statement"].search( From 0c3c8d216b0aa8c0e8751fccf4250b0abcaad5fa Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 17 Mar 2022 19:39:31 +0100 Subject: [PATCH 20/24] [FIX] *adyen*: Test file currency should be same as on journal --- .../test_files/settlement_detail_report_batch_238.csv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv index 164262cc3..7060b966b 100644 --- a/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv +++ b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv @@ -1,4 +1,4 @@ Company Account,Merchant Account,Psp Reference,Merchant Reference,Payment Method,Creation Date,TimeZone,Type,Modification Reference,Gross Currency,Gross Debit (GC),Gross Credit (GC),Exchange Rate,Net Currency,Net Debit (NC),Net Credit (NC),Commission (NC),Markup (NC),Scheme Fees (NC),Interchange (NC),Payment Method Variant,Modification Merchant Reference,Batch Number,Reserved4,Reserved5,Reserved6,Reserved7,Reserved8,Reserved9,Reserved10 -CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-02-22 00:24:23,CET,Balancetransfer,Balancetransfer,,,,,EUR,,454331.99,,,,,,Balancetransfer from batch 237,238,,,,,,, -CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Fee,Transaction Fees March 01 2022,,,,,EUR,0.09,,,,,,,,238,,,,,,, -CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Balancetransfer,Balancetransfer,,,,,EUR,454331.90,,,,,,,Balancetransfer to batch 239,238,,,,,,, +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-02-22 00:24:23,CET,Balancetransfer,Balancetransfer,,,,,USD,,454331.99,,,,,,Balancetransfer from batch 237,238,,,,,,, +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Fee,Transaction Fees March 01 2022,,,,,USD,0.09,,,,,,,,238,,,,,,, +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Balancetransfer,Balancetransfer,,,,,USD,454331.90,,,,,,,Balancetransfer to batch 239,238,,,,,,, From 27da529086a6211e2cbe25c378ca5b02e4fa92e7 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Fri, 18 Mar 2022 00:11:13 +0100 Subject: [PATCH 21/24] [FIX] *_paypal: remove external dependency csv csv is part of the standard library. Defining this as an external dependency crashes runboat --- .../models/account_bank_statement_import_paypal_parser.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py b/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py index 395ff7065..791fda954 100644 --- a/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py +++ b/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py @@ -5,6 +5,7 @@ import itertools import logging +from csv import reader from datetime import datetime from decimal import Decimal from io import StringIO @@ -16,11 +17,6 @@ _logger = logging.getLogger(__name__) -try: - from csv import reader -except (ImportError, IOError) as err: - _logger.error(err) - class AccountBankStatementImportPayPalParser(models.TransientModel): _name = "account.bank.statement.import.paypal.parser" From 1ef50a73ba7039ba2d5973751cae5402a08d4992 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Fri, 18 Mar 2022 16:05:29 +0100 Subject: [PATCH 22/24] [IMP] *_import_adyen: more information retained Put more information in the generated statement line. Also put the for users more recognizable merchant ref in the name (Label) field, and put the psp reference in ref. With a small refactoring the information retrieved can also more easily be customized. --- .../models/account_bank_statement_import.py | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) 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 dffb4439f..c4b938e0d 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 @@ -54,10 +54,10 @@ def _parse_file(self, data_file): try: _logger.debug(_("Try parsing as Adyen settlement details.")) return self._parse_adyen_file(data_file) - except Exception: # pylint: disable=broad-except + except Exception as exc: # pylint: disable=broad-except message = _("Statement file was not a Adyen settlement details file.") if self.env.context.get("account_bank_statement_import_adyen", False): - raise UserError(message) + raise UserError(message) from exc _logger.debug(message, exc_info=True) return super()._parse_file(data_file) @@ -123,7 +123,9 @@ def _parse_adyen_file(self, data_file): else: balance += self._balance(row) num_transactions += 1 - self._import_adyen_transaction(statement, row) + transaction = self._get_adyen_transaction(row) + transaction["unique_import_id"] = self._get_unique_import_id(statement) + statement["transactions"].append(transaction) fees += self._sum_fees(row) if not headers: raise ValueError( @@ -233,26 +235,40 @@ def _sum_amount_values(self, row, columns): amount += float(value) return amount - def _import_adyen_transaction(self, statement, row): - """Add transaction from row to statements.""" - transaction = dict( - unique_import_id=self._get_unique_import_id(statement), - date=self._get_transaction_date(row), - amount=self._balance(row), - note="{} {} {} {}".format( - self._get_value(row, "Merchant Account"), - self._get_value(row, "Psp Reference"), - self._get_value(row, "Merchant Reference"), - self._get_value(row, "Payment Method Variant"), - ), - name="%s" - % ( - self._get_value(row, "Psp Reference") - or self._get_value(row, "Merchant Reference") - or self._get_value(row, "Modification Reference") - ), + def _get_adyen_transaction(self, row): + """Get transaction from row. + + This can easily be overwritten in custom modules to add extra information. + """ + merchant_account = self._get_value(row, "Merchant Account") + psp_reference = self._get_value(row, "Psp Reference") + merchant_reference = self._get_value(row, "Merchant Reference") + payment_method = self._get_value(row, "Payment Method Variant") + modification_reference = self._get_value(row, "Modification Reference") + transaction = { + "date": self._get_transaction_date(row), + "amount": self._balance(row), + } + transaction["note"] = " ".join( + [ + part + for part in [ + merchant_account, + psp_reference, + merchant_reference, + payment_method, + ] + if part + ] ) - statement["transactions"].append(transaction) + transaction["name"] = ( + merchant_reference or psp_reference or modification_reference + ) + transaction["ref"] = ( + psp_reference or modification_reference or merchant_reference + ) + transaction["transaction_type"] = self._get_value(row, "Type") + return transaction def _get_unique_import_id(self, statement): """get unique import ID for transaction.""" From 6a758133108133e5192423f96c91ecd9f7b4ff7a Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Mon, 28 Mar 2022 12:46:39 +0200 Subject: [PATCH 23/24] [IMP] *_adyen: Check merchant account as early as possible --- .../models/account_bank_statement_import.py | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) 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 c4b938e0d..ea3674b3a 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 @@ -61,24 +61,6 @@ def _parse_file(self, data_file): _logger.debug(message, exc_info=True) return super()._parse_file(data_file) - def _find_additional_data(self, currency_code, account_number): - """Check if journal passed in the context matches Adyen Merchant Account.""" - if account_number: - journal = self.env["account.journal"].search( - [("adyen_merchant_account", "=", account_number)], limit=1 - ) - if journal: - 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 - ) - return super()._find_additional_data(currency_code, account_number) - def _parse_adyen_file(self, data_file): """Parse file assuming it is an Adyen file. @@ -111,10 +93,11 @@ def _parse_adyen_file(self, data_file): "less then minimum of 24" % len(row) ) if not statement: + merchant_account = self._get_value(row, "Merchant Account") + self._validate_merchant_account(merchant_account) batch_number = self._get_value(row, "Batch Number") statement = self._make_statement(row) currency_code = self._get_value(row, "Net Currency") - merchant_id = self._get_value(row, "Merchant Account") else: self._update_statement(statement, row) row_type = self._get_value(row, "Type").strip() @@ -147,7 +130,26 @@ def _parse_adyen_file(self, data_file): num_rows, num_transactions, ) - return currency_code, merchant_id, [statement] + return currency_code, merchant_account, [statement] + + def _validate_merchant_account(self, merchant_account): + """Check wether merchant account exist, and belongs to the correct journal.""" + journal = self.env["account.journal"].search( + [("adyen_merchant_account", "=", merchant_account)], limit=1 + ) + if not journal: + raise UserError( + _("No journal refers to Merchant Account %s") % merchant_account + ) + 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" + ) + % merchant_account + ) def _get_rows(self, data_file): """Get rows from data_file.""" From 943d783b68e714edd6b94f4ec89a8b0850f6aec8 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Mon, 28 Mar 2022 17:58:54 +0200 Subject: [PATCH 24/24] [RFR] *_import_adyen: refactor Move parsing code to separate class; Break up large and unwieldy parsing method in separate methods. --- .../models/__init__.py | 1 + .../models/account_bank_statement_import.py | 250 +-------------- ...ount_bank_statement_import_adyen_parser.py | 286 ++++++++++++++++++ 3 files changed, 292 insertions(+), 245 deletions(-) create mode 100644 account_bank_statement_import_adyen/models/account_bank_statement_import_adyen_parser.py diff --git a/account_bank_statement_import_adyen/models/__init__.py b/account_bank_statement_import_adyen/models/__init__.py index ba1f49342..7ce2f087d 100644 --- a/account_bank_statement_import_adyen/models/__init__.py +++ b/account_bank_statement_import_adyen/models/__init__.py @@ -1,2 +1,3 @@ from . import account_bank_statement_import +from . import account_bank_statement_import_adyen_parser from . import account_journal 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 ea3674b3a..0e232a337 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 @@ -5,44 +5,11 @@ # pylint: disable=protected-access,no-self-use import logging -from odoo import _, fields, models +from odoo import _, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) # pylint: disable=invalid-name -COLUMNS = { - "Company Account": 1, - "Merchant Account": 2, - "Psp Reference": 3, - "Merchant Reference": 4, - "Payment Method": 5, # Not used at present - "Creation Date": 6, - "TimeZone": 7, # Not used at present - "Type": 8, - "Modification Reference": 9, - "Gross Currency": 10, # Not used at present - "Gross Debit (GC)": 11, # Not used at present - "Gross Credit (GC)": 12, # Not used at present - "Exchange Rate": 13, # Not used at present - "Net Currency": 14, - "Net Debit (NC)": 15, # Fee or Merchant Payout - "Net Credit (NC)": 16, - "Commission (NC)": 17, - "Markup (NC)": 18, - "Scheme Fees (NC)": 19, - "Interchange (NC)": 20, - "Payment Method Variant": 21, - "Modification Merchant Reference": 22, # Not used at present - "Batch Number": 23, - "Reserved4": 24, # Not used at present - "Reserved5": 25, # Not used at present - "Reserved6": 26, # Not used at present - "Reserved7": 27, # Not used at present - "Reserved8": 28, # Not used at present - "Reserved9": 29, # Not used at present - "Reserved10": 30, # Not used at present -} - class AccountBankStatementImport(models.TransientModel): """Add import of Adyen statements.""" @@ -52,7 +19,6 @@ class AccountBankStatementImport(models.TransientModel): def _parse_file(self, data_file): """Parse an Adyen xlsx file and map merchant account strings to journals.""" try: - _logger.debug(_("Try parsing as Adyen settlement details.")) return self._parse_adyen_file(data_file) except Exception as exc: # pylint: disable=broad-except message = _("Statement file was not a Adyen settlement details file.") @@ -62,94 +28,11 @@ def _parse_file(self, data_file): return super()._parse_file(data_file) def _parse_adyen_file(self, data_file): - """Parse file assuming it is an Adyen file. - - An Exception will be thrown if file cannot be parsed. - """ - # pylint: disable=too-many-locals,too-many-branches - statement = None - headers = False - batch_number = False - fees = 0.0 - balance = 0.0 - payout = 0.0 + """Just parse the adyen file.""" + _logger.debug(_("Try parsing as Adyen settlement details.")) + parser = self.env["account.bank.statement.import.adyen.parser"] rows = self._get_rows(data_file) - num_rows = 0 - num_transactions = 0 - for row in rows: - num_rows += 1 - if not row[1]: - continue - if not headers: - on_header_row = self._check_header_row(row) - if not on_header_row: - continue - self._set_columns(row) - headers = True - continue - if len(row) < 24: - raise ValueError( - "Not an Adyen statement. Unexpected row length %s " - "less then minimum of 24" % len(row) - ) - if not statement: - merchant_account = self._get_value(row, "Merchant Account") - self._validate_merchant_account(merchant_account) - batch_number = self._get_value(row, "Batch Number") - statement = self._make_statement(row) - currency_code = self._get_value(row, "Net Currency") - else: - self._update_statement(statement, row) - row_type = self._get_value(row, "Type").strip() - if row_type == "MerchantPayout": - payout -= self._balance(row) - else: - balance += self._balance(row) - num_transactions += 1 - transaction = self._get_adyen_transaction(row) - transaction["unique_import_id"] = self._get_unique_import_id(statement) - statement["transactions"].append(transaction) - fees += self._sum_fees(row) - if not headers: - raise ValueError( - "Not an Adyen statement. Did not encounter header row in %d rows." - % (num_rows,) - ) - if fees: - balance -= fees - self._add_fees_transaction(statement, fees, batch_number) - if statement["transactions"] and not payout: - _logger.info(_("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) - ) - _logger.info( - _("Processed %d rows from Adyen statement file with %d transactions"), - num_rows, - num_transactions, - ) - return currency_code, merchant_account, [statement] - - def _validate_merchant_account(self, merchant_account): - """Check wether merchant account exist, and belongs to the correct journal.""" - journal = self.env["account.journal"].search( - [("adyen_merchant_account", "=", merchant_account)], limit=1 - ) - if not journal: - raise UserError( - _("No journal refers to Merchant Account %s") % merchant_account - ) - 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" - ) - % merchant_account - ) + return parser.parse_rows(rows) def _get_rows(self, data_file): """Get rows from data_file.""" @@ -162,126 +45,3 @@ def _get_rows(self, data_file): import_model = self.env["base_import.import"] importer = import_model.create({"file": data_file, "file_name": filename}) return importer._read_file({"quoting": '"', "separator": ","}) - - def _check_header_row(self, row): - """Header row is the first one with a "Company Account" header cell.""" - for cell in row: - if cell == "Company Account": - return True - return False - - def _set_columns(self, row): - """Set columns from headers. There MUST be a 'Company Account' header.""" - seen_company_account = False - for num, header in enumerate(row): - if not header.strip(): - continue # Ignore empty columns. - if header == "Company Account": - seen_company_account = True - if header not in COLUMNS: - _logger.debug(_("Unknown header %s in Adyen statement headers"), header) - else: - COLUMNS[header] = num # Set the right number for the column. - if not seen_company_account: - raise ValueError( - _("Not an Adyen statement. Headers %s do not contain 'Company Account'") - % ", ".join(row) - ) - - def _get_value(self, row, column): - """Get the value from the righ column in the row.""" - return row[COLUMNS[column]] - - def _make_statement(self, row): - """Make statement on first transaction in file.""" - statement = {"transactions": []} - statement["name"] = "{merchant} {year}/{batch}".format( - merchant=self._get_value(row, "Merchant Account"), - year=self._get_value(row, "Creation Date")[:4], - batch=self._get_value(row, "Batch Number"), - ) - statement["date"] = self._get_transaction_date(row) - return statement - - def _get_transaction_date(self, row): - """Get transaction date in right format.""" - return fields.Date.from_string(self._get_value(row, "Creation Date")) - - def _update_statement(self, statement, row): - """Update statement from transaction row.""" - # Statement date is date of earliest transaction in file. - date = self._get_transaction_date(row) - if date < statement.get("date"): - statement["date"] = date - - def _balance(self, row): - return ( - -self._sum_amount_values(row, ("Net Debit (NC)",)) - + self._sum_amount_values(row, ("Net Credit (NC)",)) - + self._sum_fees(row) - ) - - def _sum_fees(self, row): - """Sum the amounts in the fees columns.""" - return self._sum_amount_values( - row, - ("Commission (NC)", "Markup (NC)", "Scheme Fees (NC)", "Interchange (NC)",), - ) - - def _sum_amount_values(self, row, columns): - """Sum the amounts from the columns passed.""" - amount = 0.0 - for column in columns: - value = self._get_value(row, column) - if value: - amount += float(value) - return amount - - def _get_adyen_transaction(self, row): - """Get transaction from row. - - This can easily be overwritten in custom modules to add extra information. - """ - merchant_account = self._get_value(row, "Merchant Account") - psp_reference = self._get_value(row, "Psp Reference") - merchant_reference = self._get_value(row, "Merchant Reference") - payment_method = self._get_value(row, "Payment Method Variant") - modification_reference = self._get_value(row, "Modification Reference") - transaction = { - "date": self._get_transaction_date(row), - "amount": self._balance(row), - } - transaction["note"] = " ".join( - [ - part - for part in [ - merchant_account, - psp_reference, - merchant_reference, - payment_method, - ] - if part - ] - ) - transaction["name"] = ( - merchant_reference or psp_reference or modification_reference - ) - transaction["ref"] = ( - psp_reference or modification_reference or merchant_reference - ) - transaction["transaction_type"] = self._get_value(row, "Type") - return transaction - - def _get_unique_import_id(self, statement): - """get unique import ID for transaction.""" - return statement["name"] + str(len(statement["transactions"])).zfill(4) - - def _add_fees_transaction(self, statement, fees, batch_number): - """Single transaction for all fees in statement.""" - transaction = dict( - unique_import_id=self._get_unique_import_id(statement), - date=max(t["date"] for t in statement["transactions"]), - amount=-fees, - name="Commission, markup etc. batch %s" % batch_number, - ) - statement["transactions"].append(transaction) diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import_adyen_parser.py b/account_bank_statement_import_adyen/models/account_bank_statement_import_adyen_parser.py new file mode 100644 index 000000000..3186a4120 --- /dev/null +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import_adyen_parser.py @@ -0,0 +1,286 @@ +# Copyright 2017 Opener BV () +# Copyright 2021-2022 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +"""Add import of Adyen statements.""" +# pylint: disable=protected-access,no-self-use +import logging + +from odoo import _, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) # pylint: disable=invalid-name + +COLUMNS = { + "Company Account": 1, + "Merchant Account": 2, + "Psp Reference": 3, + "Merchant Reference": 4, + "Payment Method": 5, # Not used at present + "Creation Date": 6, + "TimeZone": 7, # Not used at present + "Type": 8, + "Modification Reference": 9, + "Gross Currency": 10, # Not used at present + "Gross Debit (GC)": 11, # Not used at present + "Gross Credit (GC)": 12, # Not used at present + "Exchange Rate": 13, # Not used at present + "Net Currency": 14, + "Net Debit (NC)": 15, # Fee or Merchant Payout + "Net Credit (NC)": 16, + "Commission (NC)": 17, + "Markup (NC)": 18, + "Scheme Fees (NC)": 19, + "Interchange (NC)": 20, + "Payment Method Variant": 21, + "Modification Merchant Reference": 22, # Not used at present + "Batch Number": 23, + "Reserved4": 24, # Not used at present + "Reserved5": 25, # Not used at present + "Reserved6": 26, # Not used at present + "Reserved7": 27, # Not used at present + "Reserved8": 28, # Not used at present + "Reserved9": 29, # Not used at present + "Reserved10": 30, # Not used at present +} + + +class AccountBankStatementImportAdyenParser(models.TransientModel): + """Parse Adyen statement files for bank import.""" + + _name = "account.bank.statement.import.adyen.parser" + _description = "Account Bank Statement Import Adyen Parser" + + def parse_rows(self, rows): + """Parse rows generated from an Adyen file. + + An Exception will be thrown if file cannot be parsed. + """ + statement = None + fees = 0.0 + balance = 0.0 + payout = 0.0 + num_rows = self._process_headers(rows) + for row in rows: + num_rows += 1 + if not self._is_transaction_row(row): + continue + if not statement: + statement = self._make_statement(row) + statement_info = self._get_statement_info(row) + row_type = self._get_value(row, "Type").strip() + if row_type == "MerchantPayout": + payout -= self._balance(row) + else: + balance += self._balance(row) + transaction = self._get_transaction(row) + self._append_transaction(statement, transaction) + fees += self._sum_fees(row) + if fees: + balance -= fees + self._append_fees_transaction( + statement, fees, statement_info["batch_number"] + ) + self._validate_statement(statement, payout, balance) + _logger.info( + _("Processed %d rows from Adyen statement file with %d transactions"), + num_rows, + len(statement["transactions"]), + ) + return ( + statement_info["currency_code"], + statement_info["merchant_account"], + [statement], + ) + + def _process_headers(self, rows): + """Process the headers in the generated rows.""" + num_rows = 0 + for row in rows: + num_rows += 1 + if not row[1]: + continue + on_header_row = self._check_header_row(row) + if not on_header_row: + continue + self._set_columns(row) + return num_rows + raise ValueError( + "Not an Adyen statement. Did not encounter header row in %d rows." + % (num_rows,) + ) + + def _is_transaction_row(self, row): + """Check wether row is a not empty and valid transaction row.""" + if not row[1]: + return False + if len(row) < 24: + raise ValueError( + "Not an Adyen statement. Unexpected row length %s " + "less then minimum of 24" % len(row) + ) + return True + + def _get_statement_info(self, row): + """Get general information for statement.""" + merchant_account = self._get_value(row, "Merchant Account") + self._validate_merchant_account(merchant_account) + batch_number = self._get_value(row, "Batch Number") + currency_code = self._get_value(row, "Net Currency") + return { + "merchant_account": merchant_account, + "batch_number": batch_number, + "currency_code": currency_code, + } + + def _validate_merchant_account(self, merchant_account): + """Check wether merchant account exist, and belongs to the correct journal.""" + journal = self.env["account.journal"].search( + [("adyen_merchant_account", "=", merchant_account)], limit=1 + ) + if not journal: + raise UserError( + _("No journal refers to Merchant Account %s") % merchant_account + ) + 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" + ) + % merchant_account + ) + + def _check_header_row(self, row): + """Header row is the first one with a "Company Account" header cell.""" + for cell in row: + if cell == "Company Account": + return True + return False + + def _set_columns(self, row): + """Set columns from headers. There MUST be a 'Company Account' header.""" + seen_company_account = False + for num, header in enumerate(row): + if not header.strip(): + continue # Ignore empty columns. + if header == "Company Account": + seen_company_account = True + if header not in COLUMNS: + _logger.debug(_("Unknown header %s in Adyen statement headers"), header) + else: + COLUMNS[header] = num # Set the right number for the column. + if not seen_company_account: + raise ValueError( + _("Not an Adyen statement. Headers %s do not contain 'Company Account'") + % ", ".join(row) + ) + + def _validate_statement(self, statement, payout, balance): + """Check wether statement valid: balanced. Log when no payout.""" + if statement["transactions"] and not payout: + _logger.info(_("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) + ) + + def _get_value(self, row, column): + """Get the value from the righ column in the row.""" + return row[COLUMNS[column]] + + def _make_statement(self, row): + """Make statement on first transaction in file.""" + statement = {"transactions": []} + statement["name"] = "{merchant} {year}/{batch}".format( + merchant=self._get_value(row, "Merchant Account"), + year=self._get_value(row, "Creation Date")[:4], + batch=self._get_value(row, "Batch Number"), + ) + statement["date"] = self._get_transaction_date(row) + return statement + + def _get_transaction_date(self, row): + """Get transaction date in right format.""" + return fields.Date.from_string(self._get_value(row, "Creation Date")) + + def _balance(self, row): + return ( + -self._sum_amount_values(row, ("Net Debit (NC)",)) + + self._sum_amount_values(row, ("Net Credit (NC)",)) + + self._sum_fees(row) + ) + + def _sum_fees(self, row): + """Sum the amounts in the fees columns.""" + return self._sum_amount_values( + row, + ("Commission (NC)", "Markup (NC)", "Scheme Fees (NC)", "Interchange (NC)",), + ) + + def _sum_amount_values(self, row, columns): + """Sum the amounts from the columns passed.""" + amount = 0.0 + for column in columns: + value = self._get_value(row, column) + if value: + amount += float(value) + return amount + + def _get_transaction(self, row): + """Get transaction from row. + + This can easily be overwritten in custom modules to add extra information. + """ + merchant_account = self._get_value(row, "Merchant Account") + psp_reference = self._get_value(row, "Psp Reference") + merchant_reference = self._get_value(row, "Merchant Reference") + payment_method = self._get_value(row, "Payment Method Variant") + modification_reference = self._get_value(row, "Modification Reference") + transaction = { + "date": self._get_transaction_date(row), + "amount": self._balance(row), + } + transaction["note"] = " ".join( + [ + part + for part in [ + merchant_account, + psp_reference, + merchant_reference, + payment_method, + ] + if part + ] + ) + transaction["name"] = ( + merchant_reference or psp_reference or modification_reference + ) + transaction["ref"] = ( + psp_reference or modification_reference or merchant_reference + ) + transaction["transaction_type"] = self._get_value(row, "Type") + return transaction + + def _append_fees_transaction(self, statement, fees, batch_number): + """Single transaction for all fees in statement.""" + max_date = max(t["date"] for t in statement["transactions"]) + transaction = { + "date": max_date, + "amount": -fees, + "name": "Commission, markup etc. batch %s" % batch_number, + } + self._append_transaction(statement, transaction) + + def _append_transaction(self, statement, transaction): + """Add transaction with unique import id to statement.""" + # Statement date is date of earliest transaction in file. + if transaction["date"] < statement.get("date"): + statement["date"] = transaction["date"] + transaction["unique_import_id"] = self._get_unique_import_id(statement) + statement["transactions"].append(transaction) + + def _get_unique_import_id(self, statement): + """get unique import ID for transaction.""" + return statement["name"] + str(len(statement["transactions"])).zfill(4)